From 3ce9fc4d138a58dc56f765a511450418e919d73d Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 4 Jan 2023 15:11:37 +0000 Subject: [PATCH 001/121] Add IS-10 support --- Development/README.md | 4 + Development/cmake/NmosCppDependencies.cmake | 62 + Development/cmake/NmosCppLibraries.cmake | 125 + Development/cmake/NmosCppTest.cmake | 1 + Development/cmake/nmos-cpp-config.cmake.in | 1 + Development/conanfile.txt | 1 + Development/cpprest/api_router.cpp | 5 + Development/cpprest/api_router.h | 3 + Development/cpprest/basic_utils.h | 37 + Development/cpprest/client_type.h | 39 + Development/cpprest/code_challenge_method.h | 26 + Development/cpprest/grant_type.h | 51 + Development/cpprest/resource_server_error.h | 44 + Development/cpprest/response_type.h | 41 + Development/cpprest/test/basic_utils_test.cpp | 28 + .../cpprest/token_endpoint_auth_method.h | 47 + Development/jwk/algorithm.h | 18 + Development/jwk/public_key_use.h | 16 + Development/jwt/nlohmann_traits.h | 110 + Development/nmos-cpp-node/config.json | 77 +- Development/nmos-cpp-node/main.cpp | 130 +- Development/nmos-cpp-registry/config.json | 34 +- Development/nmos-cpp-registry/main.cpp | 27 +- Development/nmos/api_utils.cpp | 103 + Development/nmos/api_utils.h | 23 + Development/nmos/authorization.cpp | 211 ++ Development/nmos/authorization.h | 65 + Development/nmos/authorization_behaviour.cpp | 521 +++ Development/nmos/authorization_behaviour.h | 50 + Development/nmos/authorization_handlers.cpp | 267 ++ Development/nmos/authorization_handlers.h | 135 + Development/nmos/authorization_operation.cpp | 1900 +++++++++++ Development/nmos/authorization_operation.h | 70 + .../nmos/authorization_redirect_api.cpp | 193 ++ Development/nmos/authorization_redirect_api.h | 28 + Development/nmos/authorization_scopes.h | 26 + Development/nmos/authorization_state.cpp | 164 + Development/nmos/authorization_state.h | 118 + Development/nmos/authorization_utils.cpp | 90 + Development/nmos/authorization_utils.h | 61 + Development/nmos/certificate_handlers.cpp | 57 + Development/nmos/certificate_handlers.h | 6 + Development/nmos/channelmapping_api.cpp | 8 +- Development/nmos/channelmapping_api.h | 7 +- Development/nmos/client_utils.cpp | 49 + Development/nmos/client_utils.h | 4 + Development/nmos/connection_api.cpp | 15 +- Development/nmos/connection_api.h | 4 +- .../nmos/connection_events_activation.cpp | 4 +- .../nmos/connection_events_activation.h | 8 +- Development/nmos/events_api.cpp | 8 +- Development/nmos/events_api.h | 7 +- Development/nmos/events_ws_api.cpp | 74 +- Development/nmos/events_ws_api.h | 8 +- Development/nmos/is10_schemas/is10_schemas.h | 27 + Development/nmos/is10_versions.h | 26 + Development/nmos/issuers.h | 25 + Development/nmos/json_fields.h | 64 + Development/nmos/json_schema.cpp | 74 + Development/nmos/json_schema.h | 8 + Development/nmos/jwk_utils.cpp | 387 +++ Development/nmos/jwk_utils.h | 37 + Development/nmos/jwks_uri_api.cpp | 62 + Development/nmos/jwks_uri_api.h | 23 + Development/nmos/jwt_generator.h | 18 + Development/nmos/jwt_generator_impl.cpp | 55 + Development/nmos/jwt_validator.h | 65 + Development/nmos/jwt_validator_impl.cpp | 494 +++ Development/nmos/mdns.cpp | 127 +- Development/nmos/mdns.h | 23 +- Development/nmos/node_api.cpp | 10 +- Development/nmos/node_api.h | 7 +- Development/nmos/node_api_target_handler.cpp | 7 +- Development/nmos/node_api_target_handler.h | 8 +- Development/nmos/node_behaviour.cpp | 98 +- Development/nmos/node_behaviour.h | 7 +- Development/nmos/node_resource.cpp | 3 +- Development/nmos/node_resources.cpp | 12 +- Development/nmos/node_resources.h | 2 +- Development/nmos/node_server.cpp | 21 +- Development/nmos/node_server.h | 29 +- Development/nmos/query_api.cpp | 8 +- Development/nmos/query_api.h | 7 +- Development/nmos/query_ws_api.cpp | 12 +- Development/nmos/query_ws_api.h | 8 +- Development/nmos/registration_api.cpp | 55 +- Development/nmos/registration_api.h | 7 +- Development/nmos/registry_server.cpp | 11 +- Development/nmos/registry_server.h | 11 +- Development/nmos/resource.h | 12 +- Development/nmos/scope.h | 48 + Development/nmos/settings.cpp | 2 + Development/nmos/settings.h | 79 +- Development/nmos/slog.h | 1 + Development/nmos/ws_api_utils.cpp | 52 + Development/nmos/ws_api_utils.h | 26 + Development/third_party/README.md | 4 + Development/third_party/is-10/README.md | 23 + .../APIs/schemas/auth_clients_schema.json | 21 + .../v1.0.x/APIs/schemas/auth_metadata.json | 79 + .../v1.0.x/APIs/schemas/jwks_response.json | 9 + .../v1.0.x/APIs/schemas/jwks_schema.json | 50 + .../register_client_error_response.json | 24 + .../APIs/schemas/register_client_request.json | 84 + .../schemas/register_client_response.json | 103 + .../APIs/schemas/token_error_response.json | 24 + .../v1.0.x/APIs/schemas/token_response.json | 29 + .../v1.0.x/APIs/schemas/token_schema.json | 76 + Development/third_party/jwt-cpp/README.md | 208 ++ Development/third_party/jwt-cpp/base.h | 208 ++ Development/third_party/jwt-cpp/jwt.h | 3040 +++++++++++++++++ 111 files changed, 11126 insertions(+), 125 deletions(-) create mode 100644 Development/cpprest/client_type.h create mode 100644 Development/cpprest/code_challenge_method.h create mode 100644 Development/cpprest/grant_type.h create mode 100644 Development/cpprest/resource_server_error.h create mode 100644 Development/cpprest/response_type.h create mode 100644 Development/cpprest/test/basic_utils_test.cpp create mode 100644 Development/cpprest/token_endpoint_auth_method.h create mode 100644 Development/jwk/algorithm.h create mode 100644 Development/jwk/public_key_use.h create mode 100644 Development/jwt/nlohmann_traits.h create mode 100644 Development/nmos/authorization.cpp create mode 100644 Development/nmos/authorization.h create mode 100644 Development/nmos/authorization_behaviour.cpp create mode 100644 Development/nmos/authorization_behaviour.h create mode 100644 Development/nmos/authorization_handlers.cpp create mode 100644 Development/nmos/authorization_handlers.h create mode 100644 Development/nmos/authorization_operation.cpp create mode 100644 Development/nmos/authorization_operation.h create mode 100644 Development/nmos/authorization_redirect_api.cpp create mode 100644 Development/nmos/authorization_redirect_api.h create mode 100644 Development/nmos/authorization_scopes.h create mode 100644 Development/nmos/authorization_state.cpp create mode 100644 Development/nmos/authorization_state.h create mode 100644 Development/nmos/authorization_utils.cpp create mode 100644 Development/nmos/authorization_utils.h create mode 100644 Development/nmos/is10_schemas/is10_schemas.h create mode 100644 Development/nmos/is10_versions.h create mode 100644 Development/nmos/issuers.h create mode 100644 Development/nmos/jwk_utils.cpp create mode 100644 Development/nmos/jwk_utils.h create mode 100644 Development/nmos/jwks_uri_api.cpp create mode 100644 Development/nmos/jwks_uri_api.h create mode 100644 Development/nmos/jwt_generator.h create mode 100644 Development/nmos/jwt_generator_impl.cpp create mode 100644 Development/nmos/jwt_validator.h create mode 100644 Development/nmos/jwt_validator_impl.cpp create mode 100644 Development/nmos/scope.h create mode 100644 Development/nmos/ws_api_utils.cpp create mode 100644 Development/nmos/ws_api_utils.h create mode 100644 Development/third_party/is-10/README.md create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json create mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json create mode 100644 Development/third_party/jwt-cpp/README.md create mode 100644 Development/third_party/jwt-cpp/base.h create mode 100644 Development/third_party/jwt-cpp/jwt.h diff --git a/Development/README.md b/Development/README.md index e98f03c4e..c135ade07 100644 --- a/Development/README.md +++ b/Development/README.md @@ -12,6 +12,10 @@ C++ source code and build files for the software Extensions to the [C++ REST SDK](https://github.com/Microsoft/cpprestsdk) - [detail](detail) Small general purpose utilties and header files to facilitate cross-platform development +- [jwk](jwk) + An implementation of the conversion between JSON Web Key and public key +- [jwt](jwt) + Providing the nlohmann JSON traits for JSON Web Key support using jwt-cpp library - [lldp](lldp) A simple API for LLDP and an implementation using the PCAP *pcap.h* API - [mdns](mdns) diff --git a/Development/cmake/NmosCppDependencies.cmake b/Development/cmake/NmosCppDependencies.cmake index cf3a939c6..3f4d8ca18 100644 --- a/Development/cmake/NmosCppDependencies.cmake +++ b/Development/cmake/NmosCppDependencies.cmake @@ -456,3 +456,65 @@ if(NMOS_CPP_BUILD_LLDP) list(APPEND NMOS_CPP_TARGETS PCAP) add_library(nmos-cpp::PCAP ALIAS PCAP) endif() + +# jwt library + +if(NMOS_CPP_USE_CONAN) + set(JWT_VERSION_MIN "0.5.1") + set(JWT_VERSION_CUR "0.5.1") + find_package(jwt-cpp REQUIRED) + if(NOT jwt-cpp_VERSION) + message(STATUS "Found jwt-cpp unknown version; minimum version: " ${JWT_VERSION_MIN}) + elseif(jwt-cpp_VERSION VERSION_LESS JWT_VERSION_MIN) + message(FATAL_ERROR "Found jwt-cpp version " ${jwt-cpp_VERSION} " that is lower than the minimum version: " ${JWT_VERSION_MIN}) + elseif(jwt-cpp_VERSION VERSION_GREATER JWT_VERSION_CUR) + message(STATUS "Found jwt-cpp version " ${jwt-cpp_VERSION} " that is higher than the current tested version: " ${JWT_VERSION_CUR}) + else() + message(STATUS "Found jwt-cpp version " ${jwt-cpp_VERSION}) + endif() + + add_library(jwt-cpp INTERFACE) + target_link_libraries(jwt-cpp INTERFACE jwt-cpp::jwt-cpp) +else() + set(JWT_SOURCES + ) + + set(JWT_HEADERS + third_party/jwt-cpp/jwt.h + ) + + add_library( + jwt-cpp STATIC + ${JWT_SOURCES} + ${JWT_HEADERS} + ) + + source_group("Source Files" FILES ${JWT_SOURCES}) + source_group("Header Files" FILES ${JWT_HEADERS}) + + if(CMAKE_CXX_COMPILER_ID MATCHES GNU) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) + target_compile_definitions( + jwt-cpp PRIVATE + ) + endif() + endif() + + target_link_libraries( + jwt-cpp PRIVATE + nmos-cpp::compile-settings + ) + target_include_directories(jwt-cpp PUBLIC + $ + $ + ) +endif() + +target_compile_definitions( + jwt-cpp INTERFACE + JWT_DISABLE_PICOJSON + ) + +set_target_properties(jwt-cpp PROPERTIES LINKER_LANGUAGE CXX) +list(APPEND NMOS_CPP_TARGETS jwt-cpp) +add_library(nmos-cpp::jwt-cpp ALIAS jwt-cpp) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index b6d6d972a..d6e183ef9 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -686,6 +686,81 @@ target_include_directories(nmos_is09_schemas PUBLIC list(APPEND NMOS_CPP_TARGETS nmos_is09_schemas) add_library(nmos-cpp::nmos_is09_schemas ALIAS nmos_is09_schemas) +# nmos_is10_schemas library + +set(NMOS_IS10_SCHEMAS_HEADERS + nmos/is10_schemas/is10_schemas.h + ) + +set(NMOS_IS10_V1_0_TAG v1.0.x) + +set(NMOS_IS10_V1_0_SCHEMAS_JSON + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/auth_clients_schema.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/auth_metadata.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/jwks_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/jwks_schema.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/register_client_error_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/register_client_request.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/register_client_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/token_error_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/token_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/token_schema.json + ) + +set(NMOS_IS10_SCHEMAS_JSON_MATCH "third_party/is-10/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS10_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is10_schemas/\\1/\\2.cpp") +string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS10_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS10_V1_0_SCHEMAS_SOURCES "${NMOS_IS10_V1_0_SCHEMAS_JSON}") + +foreach(JSON ${NMOS_IS10_V1_0_SCHEMAS_JSON}) + string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}" "${NMOS_IS10_SCHEMAS_SOURCE_REPLACE}" SOURCE "${JSON}") + string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}" "\\1" NS "${JSON}") + string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}" "\\2" VAR "${JSON}") + string(MAKE_C_IDENTIFIER "${NS}" NS) + string(MAKE_C_IDENTIFIER "${VAR}" VAR) + + file(WRITE "${SOURCE}.in" "\ +// Auto-generated from: ${JSON}\n\ +\n\ +namespace nmos\n\ +{\n\ + namespace is10_schemas\n\ + {\n\ + namespace ${NS}\n\ + {\n\ + const char* ${VAR} = R\"-auto-generated-(") + + file(READ "${JSON}" RAW) + file(APPEND "${SOURCE}.in" "${RAW}") + + file(APPEND "${SOURCE}.in" ")-auto-generated-\";\n\ + }\n\ + }\n\ +}\n") + + configure_file("${SOURCE}.in" "${SOURCE}" COPYONLY) +endforeach() + +add_library( + nmos_is10_schemas STATIC + ${NMOS_IS10_SCHEMAS_HEADERS} + ${NMOS_IS10_V1_0_SCHEMAS_SOURCES} + ) + +source_group("nmos\\is10_schemas\\Header Files" FILES ${NMOS_IS10_SCHEMAS_HEADERS}) +source_group("nmos\\is10_schemas\\${NMOS_IS10_V1_0_TAG}\\Source Files" FILES ${NMOS_IS10_V1_0_SCHEMAS_SOURCES}) + +target_link_libraries( + nmos_is10_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is10_schemas PUBLIC + $ + $ + ) + +list(APPEND NMOS_CPP_TARGETS nmos_is10_schemas) +add_library(nmos-cpp::nmos_is10_schemas ALIAS nmos_is10_schemas) + # nmos-cpp library set(NMOS_CPP_BST_SOURCES @@ -718,6 +793,9 @@ endif() set(NMOS_CPP_CPPREST_HEADERS cpprest/api_router.h cpprest/basic_utils.h + cpprest/client_type.h + cpprest/code_challenge_method.h + cpprest/grant_type.h cpprest/host_utils.h cpprest/http_utils.h cpprest/json_escape.h @@ -728,6 +806,9 @@ set(NMOS_CPP_CPPREST_HEADERS cpprest/json_visit.h cpprest/logging_utils.h cpprest/regex_utils.h + cpprest/resource_server_error.h + cpprest/response_type.h + cpprest/token_endpoint_auth_method.h cpprest/uri_schemes.h cpprest/ws_listener.h cpprest/ws_utils.h @@ -740,11 +821,28 @@ set(NMOS_CPP_CPPREST_DETAILS_HEADERS cpprest/details/system_error.h ) +set(NMOS_CPP_JWK_HEADERS + jwk/algorithm.h + jwk/public_key_use.h + ) + +set(NMOS_CPP_JWT_HEADERS + jwt/nlohmann_traits.h + ) + set(NMOS_CPP_NMOS_SOURCES nmos/activation_utils.cpp nmos/admin_ui.cpp nmos/api_downgrade.cpp nmos/api_utils.cpp + nmos/authorization.cpp + nmos/authorization_handlers.cpp + nmos/authorization_redirect_api.cpp + nmos/authorization_behaviour.cpp + nmos/authorization_operation.cpp + nmos/authorization_state.cpp + nmos/authorization_utils.cpp + nmos/authorization_behaviour.cpp nmos/capabilities.cpp nmos/certificate_handlers.cpp nmos/channelmapping_activation.cpp @@ -768,6 +866,10 @@ set(NMOS_CPP_NMOS_SOURCES nmos/lldp_handler.cpp nmos/lldp_manager.cpp nmos/json_schema.cpp + nmos/jwt_generator_impl.cpp + nmos/jwk_utils.cpp + nmos/jwks_uri_api.cpp + nmos/jwt_validator_impl.cpp nmos/log_model.cpp nmos/logging_api.cpp nmos/manifest_api.cpp @@ -803,6 +905,7 @@ set(NMOS_CPP_NMOS_SOURCES nmos/system_api.cpp nmos/system_resources.cpp nmos/video_jxsv.cpp + nmos/ws_api_utils.cpp ) set(NMOS_CPP_NMOS_HEADERS nmos/activation_mode.h @@ -811,6 +914,14 @@ set(NMOS_CPP_NMOS_HEADERS nmos/api_downgrade.h nmos/api_utils.h nmos/api_version.h + nmos/authorization.h + nmos/authorization_handlers.h + nmos/authorization_redirect_api.h + nmos/authorization_behaviour.h + nmos/authorization_operation.h + nmos/authorization_scopes.h + nmos/authorization_state.h + nmos/authorization_utils.h nmos/capabilities.h nmos/certificate_handlers.h nmos/certificate_settings.h @@ -846,8 +957,14 @@ set(NMOS_CPP_NMOS_HEADERS nmos/is07_versions.h nmos/is08_versions.h nmos/is09_versions.h + nmos/is10_versions.h + nmos/issuers.h nmos/json_fields.h nmos/json_schema.h + nmos/jwks_uri_api.h + nmos/jwk_utils.h + nmos/jwt_generator.h + nmos/jwt_validator.h nmos/lldp_handler.h nmos/lldp_manager.h nmos/log_gate.h @@ -886,6 +1003,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/resource.h nmos/resources.h nmos/schemas_api.h + nmos/scope.h nmos/sdp_utils.h nmos/server.h nmos/server_utils.h @@ -907,6 +1025,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/video_jxsv.h nmos/vpid_code.h nmos/websockets.h + nmos/ws_api_utils.h ) set(NMOS_CPP_PPLX_SOURCES @@ -948,6 +1067,8 @@ add_library( ${NMOS_CPP_CPPREST_HEADERS} ${NMOS_CPP_NMOS_SOURCES} ${NMOS_CPP_NMOS_HEADERS} + ${NMOS_CPP_JWT_HEADERS} + ${NMOS_CPP_JWK_HEADERS} ${NMOS_CPP_PPLX_SOURCES} ${NMOS_CPP_PPLX_HEADERS} ${NMOS_CPP_RQL_SOURCES} @@ -968,6 +1089,8 @@ source_group("ssl\\Source Files" FILES ${NMOS_CPP_SSL_SOURCES}) source_group("bst\\Header Files" FILES ${NMOS_CPP_BST_HEADERS}) source_group("cpprest\\Header Files" FILES ${NMOS_CPP_CPPREST_HEADERS}) +source_group("jwt\\Header Files" FILES ${NMOS_CPP_JWT_HEADERS}) +source_group("jwk\\Header Files" FILES ${NMOS_CPP_JWK_HEADERS}) source_group("nmos\\Header Files" FILES ${NMOS_CPP_NMOS_HEADERS}) source_group("pplx\\Header Files" FILES ${NMOS_CPP_PPLX_HEADERS}) source_group("rql\\Header Files" FILES ${NMOS_CPP_RQL_HEADERS}) @@ -984,6 +1107,7 @@ target_link_libraries( nmos-cpp::nmos_is05_schemas nmos-cpp::nmos_is08_schemas nmos-cpp::nmos_is09_schemas + nmos-cpp::nmos_is10_schemas nmos-cpp::mdns nmos-cpp::slog nmos-cpp::OpenSSL @@ -994,6 +1118,7 @@ target_link_libraries( nmos-cpp PRIVATE nmos-cpp::websocketpp nmos-cpp::json_schema_validator + nmos-cpp::jwt-cpp ) if(NMOS_CPP_BUILD_LLDP) target_link_libraries( diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 02db14903..b5f708fe4 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -14,6 +14,7 @@ set(NMOS_CPP_TEST_BST_TEST_HEADERS set(NMOS_CPP_TEST_CPPREST_TEST_SOURCES cpprest/test/api_router_test.cpp + cpprest/test/basic_utils_test.cpp cpprest/test/http_utils_test.cpp cpprest/test/json_utils_test.cpp cpprest/test/json_visit_test.cpp diff --git a/Development/cmake/nmos-cpp-config.cmake.in b/Development/cmake/nmos-cpp-config.cmake.in index 13da90ff3..b9e6254a2 100644 --- a/Development/cmake/nmos-cpp-config.cmake.in +++ b/Development/cmake/nmos-cpp-config.cmake.in @@ -11,6 +11,7 @@ find_dependency(cpprestsdk) find_dependency(OpenSSL) if(@NMOS_CPP_USE_CONAN@) find_dependency(nlohmann_json_schema_validator) + find_dependency(jwt-cpp) endif() if(@CMAKE_SYSTEM_NAME@ STREQUAL "Linux") if(@NMOS_CPP_USE_AVAHI@) diff --git a/Development/conanfile.txt b/Development/conanfile.txt index be2f189c3..6d0c58d31 100644 --- a/Development/conanfile.txt +++ b/Development/conanfile.txt @@ -6,6 +6,7 @@ openssl/1.1.1s json-schema-validator/2.2.0 nlohmann_json/3.11.2 zlib/1.2.13 +jwt-cpp/0.5.1 [imports] bin, *.dll -> ./bin diff --git a/Development/cpprest/api_router.cpp b/Development/cpprest/api_router.cpp index a86d8782d..c0bceb3f3 100644 --- a/Development/cpprest/api_router.cpp +++ b/Development/cpprest/api_router.cpp @@ -198,6 +198,11 @@ namespace web impl->insert(impl->routes.end(), details::match_prefix, route_pattern, any_method, all_handler); } + void api_router::pop_back() + { + impl->routes.pop_back(); + } + void api_router::set_exception_handler(route_handler handler) { impl->exception_handler = handler; diff --git a/Development/cpprest/api_router.h b/Development/cpprest/api_router.h index 59d18fb2d..38b53e6c0 100644 --- a/Development/cpprest/api_router.h +++ b/Development/cpprest/api_router.h @@ -59,6 +59,9 @@ namespace web // add a handler to support all other requests for this route and sub-routes (must be added after any method-specific handlers) void mount(const utility::string_t& route_pattern, route_handler all_handler); + // pop back handler + void pop_back(); + // provide an exception handler for this route and sub-routes (using std::current_exception, etc.) void set_exception_handler(route_handler handler); diff --git a/Development/cpprest/basic_utils.h b/Development/cpprest/basic_utils.h index 7f53bb239..01aa76f57 100644 --- a/Development/cpprest/basic_utils.h +++ b/Development/cpprest/basic_utils.h @@ -30,6 +30,43 @@ namespace utility return !iss.fail() ? t : default_val; } } + + /// + /// Encode the given byte array into a base64url string + /// using the alternative alphabet and skipping the padding + /// as per https://tools.ietf.org/html/rfc4648#section-5 + /// + inline utility::string_t to_base64url(const std::vector& data) + { + auto str = utility::conversions::to_base64(data); + auto it = str.begin(); + for (; str.end() != it; ++it) + { + auto& c = *it; + if (U('=') == c) break; + if (U('+') == c) c = U('-'); + else if (U('/') == c) c = U('_'); + } + str.erase(it, str.end()); + return str; + } + + /// + /// Decode the given base64url string to a byte array + /// using the alternative alphabet and skipping the padding + /// as per https://tools.ietf.org/html/rfc4648#section-5 + /// + inline std::vector from_base64url(utility::string_t str) + { + for (auto& c : str) + { + if (U('-') == c) c = U('+'); + else if (U('_') == c) c = U('/'); + } + auto m4 = str.size() % 4; + if (0 != m4) str.insert(str.end(), 4 - m4, U('=')); + return utility::conversions::from_base64(str); + } } } diff --git a/Development/cpprest/client_type.h b/Development/cpprest/client_type.h new file mode 100644 index 000000000..d72fa169b --- /dev/null +++ b/Development/cpprest/client_type.h @@ -0,0 +1,39 @@ +#ifndef CPPREST_CLIENT_TYPE_H +#define CPPREST_CLIENT_TYPE_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc6749#section-2.1 + namespace experimental + { + DEFINE_STRING_ENUM(client_type) + namespace client_types + { + const client_type confidential_client{ U("confidential_client") }; + const client_type public_client{ U("public_client") }; + } + + inline utility::string_t make_client_type(const client_type& type) + { + return type.name; + } + + inline client_type parse_client_type(const utility::string_t& type) + { + if (client_types::confidential_client.name == type) { return client_types::confidential_client; } + if (client_types::public_client.name == type) { return client_types::public_client; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/code_challenge_method.h b/Development/cpprest/code_challenge_method.h new file mode 100644 index 000000000..2fb09e541 --- /dev/null +++ b/Development/cpprest/code_challenge_method.h @@ -0,0 +1,26 @@ +#ifndef CPPREST_CODE_CHALLENGE_METHOD_H +#define CPPREST_CODE_CHALLENGE_METHOD_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + DEFINE_STRING_ENUM(code_challenge_method) + namespace code_challenge_methods + { + const code_challenge_method S256{ U("S256") }; + const code_challenge_method plain{ U("plain") }; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/grant_type.h b/Development/cpprest/grant_type.h new file mode 100644 index 000000000..d729a5219 --- /dev/null +++ b/Development/cpprest/grant_type.h @@ -0,0 +1,51 @@ +#ifndef CPPREST_GRANT_TYPE_H +#define CPPREST_GRANT_TYPE_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc7591#section-2 + namespace experimental + { + DEFINE_STRING_ENUM(grant_type) + namespace grant_types + { + const grant_type authorization_code{ U("authorization_code") }; + const grant_type implicit{ U("implicit") }; + const grant_type password{ U("password") }; + const grant_type client_credentials{ U("client_credentials") }; + const grant_type refresh_token{ U("refresh_token") }; + const grant_type urn_ietf_params_oauth_grant_type_jwt_bearer{ U("urn:ietf:params:oauth:grant-type:jwt-bearer") }; + const grant_type urn_ietf_params_oauth_grant_type_saml2_bearer{ U("urn:ietf:params:oauth:grant-type:saml2-bearer") }; + const grant_type device_code{ U("urn:ietf:params:oauth:grant-type:device_code") }; + } + + inline utility::string_t make_grant(const grant_type& grant) + { + return grant.name; + } + + inline grant_type parse_grant(const utility::string_t& grant) + { + if (grant_types::authorization_code.name == grant) { return grant_types::authorization_code; } + if (grant_types::implicit.name == grant) { return grant_types::implicit; } + if (grant_types::password.name == grant) { return grant_types::password; } + if (grant_types::client_credentials.name == grant) { return grant_types::client_credentials; } + if (grant_types::refresh_token.name == grant) { return grant_types::refresh_token; } + if (grant_types::urn_ietf_params_oauth_grant_type_jwt_bearer.name == grant) { return grant_types::urn_ietf_params_oauth_grant_type_jwt_bearer; } + if (grant_types::urn_ietf_params_oauth_grant_type_saml2_bearer.name == grant) { return grant_types::urn_ietf_params_oauth_grant_type_saml2_bearer; } + if (grant_types::device_code.name == grant) { return grant_types::device_code; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/resource_server_error.h b/Development/cpprest/resource_server_error.h new file mode 100644 index 000000000..2dc599b59 --- /dev/null +++ b/Development/cpprest/resource_server_error.h @@ -0,0 +1,44 @@ +#ifndef CPPREST_RESOURCE_SERVER_ERROR_H +#define CPPREST_RESOURCE_SERVER_ERROR_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + // "When a request fails, the resource server responds using the + // appropriate HTTP status code (typically, 400, 401, 403, or 405) and + // includes one of the following error codes in the response:" + // see https://tools.ietf.org/html/rfc6750#section-3.1 + DEFINE_STRING_ENUM(resource_server_error) + namespace resource_server_errors + { + const resource_server_error invalid_request{ U("invalid_request") }; + const resource_server_error invalid_token{ U("invalid_token") }; + const resource_server_error insufficient_scope{ U("insufficient_scope") }; + } + + inline utility::string_t make_resource_server_error(const resource_server_error& error) + { + return error.name; + } + + inline resource_server_error parse_resource_server_error(const utility::string_t& error) + { + if (resource_server_errors::invalid_request.name == error) { return resource_server_errors::invalid_request; } + if (resource_server_errors::invalid_token.name == error) { return resource_server_errors::invalid_token; } + if (resource_server_errors::insufficient_scope.name == error) { return resource_server_errors::insufficient_scope; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/response_type.h b/Development/cpprest/response_type.h new file mode 100644 index 000000000..27347a2a0 --- /dev/null +++ b/Development/cpprest/response_type.h @@ -0,0 +1,41 @@ +#ifndef CPPREST_RESPONSE_TYPE_H +#define CPPREST_RESPONSE_TYPE_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc7591#section-2 + namespace experimental + { + DEFINE_STRING_ENUM(response_type) + namespace response_types + { + const response_type none{ U("none") }; + const response_type code{ U("code") }; + const response_type token{ U("token") }; + } + + inline utility::string_t make_response_type(const response_type& response_type) + { + return response_type.name; + } + + inline response_type parse_response_type(const utility::string_t& response_type) + { + if (response_types::none.name == response_type) { return response_types::none; } + if (response_types::code.name == response_type) { return response_types::code; } + if (response_types::token.name == response_type) { return response_types::token; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/test/basic_utils_test.cpp b/Development/cpprest/test/basic_utils_test.cpp new file mode 100644 index 000000000..785912e42 --- /dev/null +++ b/Development/cpprest/test/basic_utils_test.cpp @@ -0,0 +1,28 @@ +// The first "test" is of course whether the header compiles standalone +#include "cpprest/basic_utils.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testBase64Url) +{ + // See https://tools.ietf.org/html/rfc4648#section-10 + const std::pair tests[] = { + { "", "" }, + { "f", "Zg" }, + { "fo", "Zm8" }, + { "foo", "Zm9v" }, + { "foob", "Zm9vYg" }, + { "fooba", "Zm9vYmE" }, + { "foobar", "Zm9vYmFy" }, + { "???~~~", "Pz8_fn5-" } + }; + + for (const auto& test : tests) + { + const std::vector data(test.first.begin(), test.first.end()); + const utility::string_t str(test.second.begin(), test.second.end()); + BST_REQUIRE_STRING_EQUAL(str, utility::conversions::to_base64url(data)); + BST_REQUIRE_EQUAL(data, utility::conversions::from_base64url(str)); + } +} diff --git a/Development/cpprest/token_endpoint_auth_method.h b/Development/cpprest/token_endpoint_auth_method.h new file mode 100644 index 000000000..c6267f351 --- /dev/null +++ b/Development/cpprest/token_endpoint_auth_method.h @@ -0,0 +1,47 @@ +#ifndef CPPREST_TOKEN_ENDPOINT_AUTH_METHOD_H +#define CPPREST_TOKEN_ENDPOINT_AUTH_METHOD_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc7591#section-2 + namespace experimental + { + DEFINE_STRING_ENUM(token_endpoint_auth_method) + namespace token_endpoint_auth_methods + { + const token_endpoint_auth_method none{ U("none") }; + const token_endpoint_auth_method client_secret_post{ U("client_secret_post") }; + const token_endpoint_auth_method client_secret_basic{ U("client_secret_basic") }; + // openid support + // see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + const token_endpoint_auth_method private_key_jwt{ U("private_key_jwt") }; + const token_endpoint_auth_method client_secret_jwt{ U("client_secret_jwt") }; + } + + inline utility::string_t make_token_endpoint_auth_method(const token_endpoint_auth_method& token_endpoint_auth_method) + { + return token_endpoint_auth_method.name; + } + + inline token_endpoint_auth_method parse_token_endpoint_auth_method(const utility::string_t& token_endpoint_auth_method) + { + if (token_endpoint_auth_method == token_endpoint_auth_methods::client_secret_basic.name) { return token_endpoint_auth_methods::client_secret_basic; } + if (token_endpoint_auth_method == token_endpoint_auth_methods::client_secret_post.name) { return token_endpoint_auth_methods::client_secret_post; } + if (token_endpoint_auth_method == token_endpoint_auth_methods::none.name) { return token_endpoint_auth_methods::none; } + if (token_endpoint_auth_method == token_endpoint_auth_methods::private_key_jwt.name) { return token_endpoint_auth_methods::private_key_jwt; } + if (token_endpoint_auth_method == token_endpoint_auth_methods::client_secret_jwt.name) { return token_endpoint_auth_methods::client_secret_jwt; } + return {}; + } + } + } + } +} + +#endif diff --git a/Development/jwk/algorithm.h b/Development/jwk/algorithm.h new file mode 100644 index 000000000..a9d82d35c --- /dev/null +++ b/Development/jwk/algorithm.h @@ -0,0 +1,18 @@ +#ifndef JWK_ALGORITHM_H +#define JWK_ALGORITHM_H + +#include "nmos/string_enum.h" + +namespace jwk +{ + DEFINE_STRING_ENUM(algorithm) + namespace algorithms + { + // RS256/RS384/RS512 + const algorithm RS256{ U("RS256") }; + const algorithm RS384{ U("RS384") }; + const algorithm RS512{ U("RS512") }; + } +} + +#endif diff --git a/Development/jwk/public_key_use.h b/Development/jwk/public_key_use.h new file mode 100644 index 000000000..ac9ca2c9c --- /dev/null +++ b/Development/jwk/public_key_use.h @@ -0,0 +1,16 @@ +#ifndef JWK_PUBLICKEY_USE_H +#define JWK_PUBLICKEY_USE_H + +#include "nmos/string_enum.h" + +namespace jwk +{ + DEFINE_STRING_ENUM(public_key_use) + namespace public_key_uses + { + const public_key_use signing{ U("sig") }; + const public_key_use encryption{ U("enc") }; + } +} + +#endif diff --git a/Development/jwt/nlohmann_traits.h b/Development/jwt/nlohmann_traits.h new file mode 100644 index 000000000..a2817c6bb --- /dev/null +++ b/Development/jwt/nlohmann_traits.h @@ -0,0 +1,110 @@ +#ifndef JWT_NLOHMANN_H +#define JWT_NLOHMANN_H + +#include "jwt-cpp/jwt.h" +#include "nlohmann/json.hpp" + +namespace jwt +{ + namespace experimental + { + namespace details + { + struct nlohmann_traits { + using json = nlohmann::json; + using value_type = json; + using object_type = json::object_t; + using array_type = json::array_t; + using string_type = std::string; // current limitation of traits implementation + using number_type = json::number_float_t; + using integer_type = json::number_integer_t; + using boolean_type = json::boolean_t; + + static jwt::json::type get_type(const json &val) + { + using jwt::json::type; + + if (val.type() == json::value_t::boolean) { return type::boolean; } + else if (val.type() == json::value_t::number_integer) { return type::integer; } + // nlohmann internally tracks two types of integers + else if (val.type() == json::value_t::number_unsigned) { return type::integer; } + else if (val.type() == json::value_t::number_float) { return type::number; } + else if (val.type() == json::value_t::string) { return type::string; } + else if (val.type() == json::value_t::array) { return type::array; } + else if (val.type() == json::value_t::object) { return type::object; } + else { throw std::logic_error("invalid type"); } + } + + static json::object_t as_object(const json &val) + { + if (val.type() != json::value_t::object) + { + throw std::bad_cast(); + } + return val.get(); + } + + static std::string as_string(const json &val) + { + if (val.type() != json::value_t::string) + { + throw std::bad_cast(); + } + return val.get(); + } + + static json::array_t as_array(const json &val) + { + if (val.type() != json::value_t::array) + { + throw std::bad_cast(); + } + return val.get(); + } + + static int64_t as_int(const json &val) + { + switch (val.type()) + { + case json::value_t::number_integer: + case json::value_t::number_unsigned: + return val.get(); + default: + throw std::bad_cast(); + } + } + + static bool as_bool(const json &val) + { + if (val.type() != json::value_t::boolean) + { + throw std::bad_cast(); + } + return val.get(); + } + + static double as_number(const json &val) + { + if (val.type() != json::value_t::number_float) + { + throw std::bad_cast(); + } + return val.get(); + } + + static bool parse(json &val, std::string str) + { + val = json::parse(str.begin(), str.end()); + return true; + } + + static std::string serialize(const json &val) + { + return val.dump(); + } + }; + } + } +} + +#endif diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index ce83f8ee5..2b1f6fc33 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -91,15 +91,22 @@ // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration //"is09_versions": ["v1.0"], + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + //"is10_versions": ["v1.0"], + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely //"pri": 100, - // highest_pri, lowest_pri [node]: used to specify the (inclusive) range of suitable 'pri' values of discovered APIs, to avoid development and live systems colliding + // highest_pri, lowest_pri [node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Registration and System APIs, to avoid development and live systems colliding //"highest_pri": 0, //"lowest_pri": 2147483647, + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + //"authorization_highest_pri": 0, + //"authorization_lowest_pri": 2147483647, + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances - // e.g. Registration APIs, System APIs, or OCSP servers + // e.g. Registration APIs, System APIs, Authorization APIs or OCSP servers //"discovery_backoff_min": 1, //"discovery_backoff_max": 30, //"discovery_backoff_factor": 1.5, @@ -263,5 +270,71 @@ // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server //"ocsp_request_max": 30, + // authorization_address [registry, node]: IP address or host name used to construct request URLs for the Authorization API (if not discovered via DNS-SD) + //"authorization_address": ip-address-string, + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + //"authorization_port" 443, + + // authorization_version [registry, node]: used to construct request URLs for Authorization API (if not discovered via DNS-SD) + //"authorization_version": "v1.0", + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + //"authorization_selector", "", + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + //"authorization_request_max": 30, + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + //"fetch_authorization_public_keys_interval_min": 3600, + //"fetch_authorization_public_keys_interval_max": 3660, + + // access_token_refresh_interval [node]: time interval (in seconds) to refresh access token from Authorization Server + // It specified the access token refresh period otherwise Bearer token's expires_in is used instead. + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#access-token-lifetime + //"access_token_refresh_interval": -1, + + // client_authorization [node]: whether clients should use authorization to access protected APIs + //"client_authorization": false, + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + //"server_authorization": false, + + // authorization_code_flow_max [node]: timeout for the authorization code workflow (in seconds) + // No timeout if value is set to -1, default to 30 seconds + //"authorization_code_flow_max": 30, + + // authorization_flow [node]: used to specify the authorization flow for the registered scopes + // supported flow are authorization_code and client_credentials + // client_credentials SHOULD only be used for NO user interface node, otherwise authorization_code MUST be used + //"authorization_flow": "authorization_code", + + // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration + //"authorization_redirect_port": 3218, + + // initial_access_token [node]: initial access token giving access to the client registration endpoint for non-opened registration + //"initial_access_token", "", + + // authorization_scopes [node]: used to specify the supported scopes for client registration + // supported scopes are registration, query, node, connection, events and channelmapping + //"authorization_scopes": array-of-scope-string, + + // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint + // supported methods are none, client_secret_basic and private_key_jwt, default to client_secret_basic, where none is used for public client + //"token_endpoint_auth_method": "client_secret_basic", + + // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration + //"jwks_uri_port": 3219, + + // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) + //"validate_openid_client": true, + + // no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks + // as it is because not all Authorization server can cope with URL with trailing dot, default to true + //"no_trailing_dot_for_authorization_callback_uri": true, + "don't worry": "about trailing commas" } diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index d2b65923f..5cba94c5c 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -1,5 +1,12 @@ #include #include +#include "cpprest/grant_type.h" +#include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_utils.h" // for make_api_listener +#include "nmos/authorization_behaviour.h" +#include "nmos/authorization_redirect_api.h" +#include "nmos/authorization_state.h" +#include "nmos/jwks_uri_api.h" #include "nmos/log_gate.h" #include "nmos/model.h" #include "nmos/node_server.h" @@ -8,6 +15,7 @@ #include "nmos/ocsp_state.h" #include "nmos/process_utils.h" #include "nmos/server.h" +#include "nmos/server_utils.h" // for make_http_listener_config #include "node_implementation.h" int main(int argc, char* argv[]) @@ -107,20 +115,34 @@ int main(int argc, char* argv[]) } #endif +// only implement communication with Authorization server if IS-10 is required +// cf. preprocessor conditions in nmos::make_node_api, nmos::make_connection_api, nmos::make_events_api, nmos::make_channelmapping_api, make_events_ws_validate_handler +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + nmos::experimental::authorization_state authorization_state; + if (nmos::experimental::fields::server_authorization(node_model.settings)) + { + node_implementation + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(node_model, authorization_state, gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(node_model, authorization_state, gate)); + } +#endif +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + if (nmos::experimental::fields::client_authorization(node_model.settings)) + { + node_implementation + .on_get_authorization_bearer_token(nmos::experimental::make_authorization_token_handler(authorization_state, gate)) + .on_make_authorization_config(nmos::experimental::make_authorization_config_handler(authorization_state, gate)) + .on_load_authorization_clients(nmos::experimental::make_load_authorization_clients_handler(node_model.settings, gate)) + .on_save_authorization_client(nmos::experimental::make_save_authorization_client_handler(node_model.settings, gate)) + .on_load_rsa_private_keys(nmos::make_load_rsa_private_keys_handler(node_model.settings, gate)) // may be omitted, only required for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint + .on_request_authorization_code(nmos::experimental::make_request_authorization_code_handler(gate)); // may be omitted, only required for OAuth client which is using the Authorization Code Flow to obtain the access token + } +#endif + // Set up the node server auto node_server = nmos::experimental::make_node_server(node_model, node_implementation, log_model, gate); - if (!nmos::experimental::fields::http_trace(node_model.settings)) - { - // Disable TRACE method - - for (auto& http_listener : node_server.http_listeners) - { - http_listener.support(web::http::methods::TRCE, [](web::http::http_request req) { req.reply(web::http::status_codes::MethodNotAllowed); }); - } - } - // Add the underlying implementation, which will set up the node resources, etc. node_server.thread_functions.push_back([&] { node_implementation_thread(node_model, gate); }); @@ -136,6 +158,94 @@ int main(int argc, char* argv[]) } #endif +// only implement communication with Authorization server if IS-10 is required +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + if (nmos::experimental::fields::client_authorization(node_model.settings)) + { + std::map api_routers; + + // Configure the authorization_redirect API (require for Authorization Code Flow support) + + if (web::http::oauth2::experimental::grant_types::authorization_code.name == nmos::experimental::fields::authorization_flow(node_model.settings)) + { + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + api_routers[{ {}, nmos::experimental::fields::authorization_redirect_port(node_model.settings) }].mount({}, nmos::experimental::make_authorization_redirect_api(node_model, authorization_state, load_ca_certificates, load_rsa_private_keys, gate)); + } + + // Configure the jwks_uri API (require for Private Key JWK support) + + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == nmos::experimental::fields::token_endpoint_auth_method(node_model.settings)) + { + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + api_routers[{ {}, nmos::experimental::fields::jwks_uri_port(node_model.settings) }].mount({}, nmos::experimental::make_jwk_uri_api(node_model, load_rsa_private_keys, gate)); + } + + auto http_config = nmos::make_http_listener_config(node_model.settings, node_implementation.load_server_certificates, node_implementation.load_dh_param, node_implementation.get_ocsp_response, gate); + const auto server_secure = nmos::experimental::fields::server_secure(node_model.settings); + const auto hsts = nmos::experimental::get_hsts(node_model.settings); + for (auto& api_router : api_routers) + { + auto found = node_server.api_routers.find(api_router.first); + + const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard; + const auto& port = nmos::experimental::server_port(api_router.first.second, node_model.settings); + + if (node_server.api_routers.end() != found) + { + const auto uri = web::http::experimental::listener::make_listener_uri(server_secure, host, port); + auto listener = std::find_if(node_server.http_listeners.begin(), node_server.http_listeners.end(), [&](const web::http::experimental::listener::http_listener& listener) { return listener.uri() == uri; }); + if (node_server.http_listeners.end() != listener) + { + found->second.pop_back(); // remove the api_finally_handler which was previously added in the make_node_server, the api_finally_handler will be re-inserted in the make_api_listener + node_server.http_listeners.erase(listener); + } + found->second.mount({}, api_router.second); + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, port, found->second, http_config, hsts, gate)); + } + else + { + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, port, api_router.second, http_config, hsts, gate)); + } + } + } +#endif + + if (!nmos::experimental::fields::http_trace(node_model.settings)) + { + // Disable TRACE method + + for (auto& http_listener : node_server.http_listeners) + { + http_listener.support(web::http::methods::TRCE, [](web::http::http_request req) { req.reply(web::http::status_codes::MethodNotAllowed); }); + } + } + +// only implement communication with Authorization server if IS-10 is required +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + if (nmos::experimental::fields::client_authorization(node_model.settings) || nmos::experimental::fields::server_authorization(node_model.settings)) + { + // IS-10 client registration, fetch access token, and fetch authorization server token public key + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html + // and https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + auto load_authorization_clients = node_implementation.load_authorization_clients; + auto save_authorization_client = node_implementation.save_authorization_client; + auto request_authorization_code = node_implementation.request_authorization_code; + node_server.thread_functions.push_back([&, load_ca_certificates, load_rsa_private_keys, load_authorization_clients, save_authorization_client, request_authorization_code] { nmos::experimental::authorization_behaviour_thread(node_model, authorization_state, load_ca_certificates, load_rsa_private_keys, load_authorization_clients, save_authorization_client, request_authorization_code, gate); }); + + if (nmos::experimental::fields::server_authorization(node_model.settings)) + { + // When no matching public key for a given access token, it SHOULD attempt to obtain the missing public key + // via the the token iss claim as specified in RFC 8414 section 3. + // see https://tools.ietf.org/html/rfc8414#section-3 + // and https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + node_server.thread_functions.push_back([&, load_ca_certificates] { nmos::experimental::authorization_token_issuer_thread(node_model, authorization_state, load_ca_certificates, gate); }); + } + } +#endif + // Open the API ports and start up node operation (including the DNS-SD advertisements) slog::log(gate, SLOG_FLF) << "Preparing for connections"; diff --git a/Development/nmos-cpp-registry/config.json b/Development/nmos-cpp-registry/config.json index 48312edeb..d53c97d64 100644 --- a/Development/nmos-cpp-registry/config.json +++ b/Development/nmos-cpp-registry/config.json @@ -34,11 +34,18 @@ // is09_versions [registry, node]: used to specify the enabled API versions (advertised via 'api_ver') for a version-locked configuration //"is09_versions": ["v1.0"], + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + //"is10_versions": ["v1.0"], + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely //"pri": 100, + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + //"authorization_highest_pri": 0, + //"authorization_lowest_pri": 2147483647, + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances - // e.g. Registration APIs, System APIs, or OCSP servers + // e.g. Registration APIs, System APIs, Authorization APIs, or OCSP servers //"discovery_backoff_min": 1, //"discovery_backoff_max": 30, //"discovery_backoff_factor": 1.5, @@ -227,5 +234,30 @@ // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server //"ocsp_request_max": 30, + // authorization_address [registry, node]: IP address or host name used to construct request URLs for the Authorization API (if not discovered via DNS-SD) + //"authorization_address": ip-address-string, + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + //"authorization_port" 443, + + // authorization_version [registry, node]: used to construct request URLs for Authorization API (if not discovered via DNS-SD) + //"authorization_version": "v1.0", + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + //"authorization_selector", "", + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + //"authorization_request_max": 30, + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + //"fetch_authorization_public_keys_interval_min": 3600, + //"fetch_authorization_public_keys_interval_max": 3660, + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + //"server_authorization": false, + "don't worry": "about trailing commas" } diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index df15bf185..010391388 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -1,5 +1,7 @@ #include #include +#include "nmos/authorization_behaviour.h" +#include "nmos/authorization_state.h" #include "nmos/log_gate.h" #include "nmos/model.h" #include "nmos/ocsp_behaviour.h" @@ -104,7 +106,20 @@ int main(int argc, char* argv[]) nmos::experimental::ocsp_state ocsp_state; if (nmos::experimental::fields::server_secure(registry_model.settings)) { - registry_implementation.on_get_ocsp_response(nmos::make_ocsp_response_handler(ocsp_state, gate)); + registry_implementation + .on_get_ocsp_response(nmos::make_ocsp_response_handler(ocsp_state, gate)); + } +#endif + +// only implement communication with Authorization server if IS-10 is required +// cf. preprocessor conditions in nmos::make_registration_api, nmos::make_query_api, make_query_ws_validate_handler +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + nmos::experimental::authorization_state authorization_state; + if (nmos::experimental::fields::server_authorization(registry_model.settings)) + { + registry_implementation + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(registry_model, authorization_state, gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(registry_model, authorization_state, gate)); } #endif @@ -135,6 +150,16 @@ int main(int argc, char* argv[]) } #endif +// only implement communication with Authorization server if IS-10 is required +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + if (nmos::experimental::fields::server_authorization(registry_model.settings)) + { + auto load_ca_certificates = registry_implementation.load_ca_certificates; + registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_behaviour_thread(registry_model, authorization_state, load_ca_certificates, {}, {}, {}, {}, gate); }); + registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_token_issuer_thread(registry_model, authorization_state, load_ca_certificates, gate); }); + } +#endif + // Open the API ports and start up registry management slog::log(gate, SLOG_FLF) << "Preparing for connections"; diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 33c385d6a..659064cd2 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -5,10 +5,16 @@ #include #include #include "cpprest/json_visit.h" +#include "cpprest/resource_server_error.h" #include "cpprest/uri_schemes.h" #include "cpprest/ws_utils.h" #include "nmos/api_version.h" +#include "nmos/authorization.h" +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" #include "nmos/media_type.h" +#include "nmos/model.h" +#include "nmos/scope.h" #include "nmos/slog.h" #include "nmos/type.h" #include "nmos/version.h" @@ -754,6 +760,103 @@ namespace nmos { return mqtt_scheme(nmos::experimental::fields::client_secure(settings)); } + + namespace experimental + { + namespace details + { + // JWT validation to confirm authentication credentials and an access token that allows access to the protected resource + // see https://tools.ietf.org/html/rfc6750#section-3 + web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, slog::base_gate& gate_) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + return [&model, &authorization_state, scope, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + if (methods::OPTIONS == req.method()) return pplx::task_from_result(true); + + const auto audience = with_read_lock(model.mutex, [&] { const auto& settings = model.settings; return nmos::get_host_name(settings); }); + auto error = with_write_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &gate_] + { + return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, authorization_state.authorization_server_uri, authorization_state.token_issuer, gate_); + }); + + if (error) + { + // set error repsonse + auto realm = web::http::get_host_port(req).first; + if (realm.empty()) { realm = nmos::get_host(model.settings); } + set_error_reply(res, realm, error); + + // if error was deal to no matching keys, trigger a re-fetch to obtain public keys from the token issuer + if (error.value == nmos::experimental::authorization_error::no_matching_keys) + { + slog::log(gate, SLOG_FLF) << "Authorization warning: " << error.message; + + with_write_lock(authorization_state.mutex, [&authorization_state] + { + authorization_state.fetch_token_issuer_pubkeys = true; + }); + + auto lock = model.write_lock(); + model.notify(); + } + else + { + slog::log(gate, SLOG_FLF) << "Authorization error: " << error.message; + } + + throw nmos::details::to_api_finally_handler{}; // in order to skip other route handlers and then send the response + } + + return pplx::task_from_result(true); + }; + } + + void set_error_reply(web::http::http_response& res, const utility::string_t& realm, const nmos::experimental::authorization_error& error) + { + using namespace web::http; + + // WWW-Authenticate Response Header Field definition + // see https://tools.ietf.org/html/rfc6750#section-3 + utility::string_t auth_params{ U("Bearer realm=") + realm }; + utility::string_t error_description{}; + // If the request lacks any authentication information (e.g., the client + // was unaware that authentication is necessary or attempted using an + // unsupported authentication method), the resource server SHOULD NOT + // include an error code or other error information. + // + // For example : + // + // HTTP / 1.1 401 Unauthorized + // WWW - Authenticate : Bearer realm = "example" + // see https://tools.ietf.org/html/rfc6750#section-3.1 + if (error.value != nmos::experimental::authorization_error::without_authentication) + { + utility::string_t error_string = { (error.value == nmos::experimental::authorization_error::insufficient_scope) ? web::http::oauth2::experimental::resource_server_errors::insufficient_scope.name : web::http::oauth2::experimental::resource_server_errors::invalid_token.name }; + error_description = utility::s2us(error.message); + auth_params += U(",error=") + error_string + U(",error_description=") + error_description; + } + + res.headers().add(web::http::header_names::www_authenticate, auth_params); + + auto status_code = status_codes::Unauthorized; + if (error.value == nmos::experimental::authorization_error::insufficient_scope) + { + status_code = status_codes::Forbidden; + } + else if (error.value == nmos::experimental::authorization_error::no_matching_keys) + { + status_code = status_codes::ServiceUnavailable; + res.headers().add(web::http::header_names::retry_after, 5); //hmm, may be a shorter retry time? + } + + nmos::set_error_reply(res, status_code, utility::s2us(error.message)); + } + } + } } #if 0 diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 2dda425a6..4959c7a8d 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -19,8 +19,14 @@ namespace slog namespace nmos { struct api_version; + struct base_model; struct type; + namespace experimental + { + struct authorization_state; + } + // Patterns are used to form parameterised route paths // (could be moved to cpprest/api_router.h or cpprest/route_pattern.h?) @@ -190,6 +196,23 @@ namespace nmos web::http::experimental::listener::route_handler make_api_finally_handler(slog::base_gate& gate); web::http::experimental::listener::route_handler make_api_finally_handler(const bst::optional& hsts, slog::base_gate& gate); } + + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + struct authorization_error; + struct scope; + + namespace details + { + // JWT validation to confirm authentication credentials and an access token that allows access to the protected resource + // see https://tools.ietf.org/html/rfc6750#section-3 + web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, slog::base_gate& gate); + + // set error response + void set_error_reply(web::http::http_response& res, const utility::string_t& realm, const nmos::experimental::authorization_error& error); + } + } } #endif diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp new file mode 100644 index 000000000..0586cf417 --- /dev/null +++ b/Development/nmos/authorization.cpp @@ -0,0 +1,211 @@ +#include "nmos/authorization.h" + +#include +#include "nmos/authorization_utils.h" +#include "nmos/jwt_validator.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + struct without_authentication_exception : std::runtime_error + { + without_authentication_exception(const std::string& message) : std::runtime_error(message) {} + }; + + bool is_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate) + { + if (access_token.empty()) + { + // no access token, treat it as expired + return true; + } + + try + { + const auto& token_issuer = nmos::experimental::jwt_validator::token_issuer(access_token); + + // is token from expected issuer + if (token_issuer == expected_issuer) + { + // is token expired + const auto& issuer = issuers.find(token_issuer); + if (issuers.end() != issuer) + { + issuer->second.jwt_validator.validate_expiry(access_token); + return false; + } + } + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "test token expiry: " << e.what(); + } + + // reaching here, token validation has failed, treat it as expired + return true; + } + + utility::string_t get_client_id(const web::http::http_headers& headers, slog::base_gate& gate) + { + try + { + const auto header = headers.find(web::http::header_names::authorization); + if (headers.end() == header) + { + throw without_authentication_exception{ "missing Authorization header" }; + } + + const auto& token = header->second; + const utility::string_t scheme{ U("Bearer ") }; + if (!boost::algorithm::starts_with(token, scheme)) + { + throw without_authentication_exception{ "unsupported authentication scheme" }; + } + + const auto access_token = token.substr(scheme.length()); + return jwt_validator::client_id(access_token); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "failed to get client_id from header: " << e.what(); + } + return{}; + } + + authorization_error validate_authorization(const utility::string_t& access_token, const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate) + { + if (access_token.empty()) + { + slog::log(gate, SLOG_FLF) << "missing access token"; + return{ authorization_error::without_authentication, "missing access token" }; + } + + if (issuers.empty()) + { + try + { + // record the unknown issuer of this access token, i.e. no public keys to validate the access token + // this will be used in the authorization_token_issuer_thread to fetch the missing public keys for token validation + token_issuer = nmos::experimental::jwt_validator::token_issuer(access_token); +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "no public keys to validate access token"; +#else + slog::log(gate, SLOG_FLF) << "no public keys to validate access token: " << access_token; +#endif + return{ authorization_error::no_matching_keys, "no public keys to validate access token" }; + } + catch (const std::exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "invalid token issuer: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "invalid token issuer: " << e.what() << "; access_token: " << access_token; +#endif + return{ authorization_error::failed, e.what() }; + } + } + + std::string error; + for (auto issuer = issuers.begin(); issuer != issuers.end(); issuer++) + { + try + { + // if jwt_validator has not already set up, treat it as no public keys to validate token + if (issuer->second.jwt_validator.is_initialized()) + { + issuer->second.jwt_validator.validate(access_token, request, scope, audience, auth_server); + return{ authorization_error::succeeded }; + } + } + catch (const no_matching_keys_exception& e) + { + // validator failed to decode token due to no valid public keys, try next set of issuer's validator + // this will be used in the authorization_token_issuer_thread to fetch the missing public keys for token validation + token_issuer = e.issuer; + error = e.what(); +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; +#else + slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; +#endif + } + catch (const insufficient_scope_exception& e) + { + // validator can decode the token, but insufficient scope +#if !defined (NDEBUG) + slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; +#endif + return{ authorization_error::insufficient_scope, e.what() }; + } + catch (const std::exception& e) + { + // validator can decode the token, with general failure +#if !defined (NDEBUG) + slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; +#endif + return{ authorization_error::failed, e.what() }; + } + } + + // reach here must be because there are no public keys to validate token + return{ authorization_error::no_matching_keys, error }; + } + + authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate) + { + try + { + const auto& headers = request.headers(); + + const auto header = headers.find(web::http::header_names::authorization); + if (headers.end() == header) + { + throw without_authentication_exception{ "missing Authorization header" }; + } + + const auto& token = header->second; + const utility::string_t scheme{ U("Bearer ") }; + if (!boost::algorithm::starts_with(token, scheme)) + { + throw without_authentication_exception{ "unsupported authentication scheme" }; + } + + const auto access_token = token.substr(scheme.length()); + return validate_authorization(access_token, issuers, request, scope, audience, auth_server, token_issuer, gate); + } + catch (const without_authentication_exception& e) + { + return{ authorization_error::without_authentication, e.what() }; + } + } + + // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket + // Clients SHOULD use the "Authorization Request Header Field" method. + // Clients MAY use "URI Query Parameter". + // See https://tools.ietf.org/html/rfc6750#section-2 + authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate) + { + auto error = validate_authorization(issuers, request, scope, audience, auth_server, token_issuer, gate); + + if (error) + { + error = { authorization_error::without_authentication, "missing access token" }; + + // test "URI Query Parameter" + const auto& query = request.request_uri().query(); + if (!query.empty()) + { + auto querys = web::uri::split_query(query); + auto it = querys.find(U("access_token")); + if (querys.end() != it) + { + error = nmos::experimental::validate_authorization(it->second, issuers, request, scope, audience, auth_server, token_issuer, gate); + } + } + } + return error; + } + } +} diff --git a/Development/nmos/authorization.h b/Development/nmos/authorization.h new file mode 100644 index 000000000..78a7e7ddf --- /dev/null +++ b/Development/nmos/authorization.h @@ -0,0 +1,65 @@ +#ifndef NMOS_AUTHORIZATION_H +#define NMOS_AUTHORIZATION_H + +#include +#include "cpprest/basic_utils.h" +#include "cpprest/json_utils.h" +#include "nmos/issuers.h" + +namespace web +{ + class uri; + + namespace http + { + class http_headers; + class http_request; + } +} + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + namespace experimental + { + struct scope; + + struct authorization_error + { + enum status_t + { + succeeded, + without_authentication, // failure: access protected resource request without authentication + insufficient_scope, // failure: access protected resource request higher privileges + no_matching_keys, // failure: no matching keys for the token validation + failed // failure: access protected resource request with authentication but failed + }; + + authorization_error() : value(without_authentication) {} + authorization_error(status_t value, const std::string& message = {}) : value(value), message(message) {} + + status_t value; + std::string message; + + operator bool() const { return succeeded != value; } + }; + + bool is_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate); + + utility::string_t get_client_id(const web::http::http_headers& headers, slog::base_gate& gate); + + authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate); + + // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket + // Clients SHOULD use the "Authorization Request Header Field" method. + // Clients MAY use "URI Query Parameter". + // See https://tools.ietf.org/html/rfc6750#section-2 + authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp new file mode 100644 index 000000000..115a6763b --- /dev/null +++ b/Development/nmos/authorization_behaviour.cpp @@ -0,0 +1,521 @@ +#include "nmos/authorization_behaviour.h" + +#include "cpprest/response_type.h" +#include "mdns/service_discovery.h" +#include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_operation.h" +#include "nmos/authorization_scopes.h" +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/is10_versions.h" +#include "nmos/model.h" +#include "nmos/random.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + namespace fields + { + const web::json::field_as_string_or ver{ U("ver"),{} }; + //const web::json::field_as_integer_or pri{ U("pri"), nmos::service_priorities::no_priority }; already defined in settings.h + const web::json::field_as_string_or uri{ U("uri"),{} }; + } + + namespace details + { + // thread to fetch token and public keys from service + void authorization_behaviour_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate); + // thread to fetch public keys from token issuer + void authorization_token_issuer_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + // background service discovery + void authorization_services_background_discovery(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate); + + // service discovery + bool discover_authorization_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + bool has_discovered_authorization_services(const nmos::base_model& model); + } + + // uses the default DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::authorization_behaviour)); + + mdns::service_discovery discovery(gate); + + details::authorization_behaviour_thread(model, authorization_state, std::move(load_ca_certificates), std::move(load_rsa_private_keys), std::move(load_authorization_clients), std::move(save_authorization_client), std::move(request_authorization_code), discovery, gate); + } + + // uses the specified DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::authorization_behaviour)); + + details::authorization_behaviour_thread(model, authorization_state, std::move(load_ca_certificates), std::move(load_rsa_private_keys), std::move(load_authorization_clients), std::move(save_authorization_client), std::move(request_authorization_code), discovery, gate); + } + + void details::authorization_behaviour_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate) + { + enum + { + initial_discovery, + request_authorization_server_metadata, + client_registration, + authorization_code_flow, + authorization_operation, + authorization_operation_with_immediate_token_fetch, + rediscovery, + background_discovery + } mode = initial_discovery; + + // If the chosen Authorization API does not respond correctly at any time, another Authorization API should be selected from the discovered list. + with_write_lock(model.mutex, [&model] { model.settings[nmos::experimental::fields::authorization_services] = web::json::value::array(); }); + + nmos::details::seed_generator discovery_backoff_seeder; + std::default_random_engine discovery_backoff_engine(discovery_backoff_seeder); + double discovery_backoff = 0; + + // load authorization clients metadata to cache + if (load_authorization_clients) + { + with_write_lock(authorization_state.mutex, [&] + { + const auto auth_clients = load_authorization_clients(); + + if (!auth_clients.is_null() && auth_clients.is_array()) + { + slog::log(gate, SLOG_FLF) << "Retrieved authorization clients: " << utility::us2s(auth_clients.serialize()) << " from non-volatile memory"; + + for (const auto auth_client : auth_clients.as_array()) + { + nmos::experimental::update_client_metadata(authorization_state, auth_client.at(nmos::experimental::fields::authorization_server_uri).as_string(), nmos::experimental::fields::client_metadata(auth_client)); + } + } + }); + } + + bool authorization_service_error{ false }; + + // continue until the server is being shut down + for (;;) + { + if (with_read_lock(model.mutex, [&] { return model.shutdown; })) break; + + switch (mode) + { + case initial_discovery: + case rediscovery: + if (0 != discovery_backoff) + { + auto lock = model.read_lock(); + const auto random_backoff = std::uniform_real_distribution<>(0, discovery_backoff)(discovery_backoff_engine); + slog::log(gate, SLOG_FLF) << "Waiting to retry Authorization API discovery for about " << std::fixed << std::setprecision(3) << random_backoff << " seconds (current backoff limit: " << discovery_backoff << " seconds)"; + model.wait_for(lock, std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * random_backoff)), [&] { return model.shutdown; }); + if (model.shutdown) break; + } + + // The Node performs a DNS-SD browse for services of type '_nmos-auth._tcp' as specified. + if (details::discover_authorization_services(model, discovery, gate)) + { + mode = request_authorization_server_metadata; + + // If unable to contact the Authorization server, we MUST implement a + // random back-off mechanism to avoid overloading the Authorization server in the event of a system restart. + auto lock = model.read_lock(); + discovery_backoff = (std::min)((std::max)((double)nmos::fields::discovery_backoff_min(model.settings), discovery_backoff * nmos::fields::discovery_backoff_factor(model.settings)), (double)nmos::fields::discovery_backoff_max(model.settings)); + } + else + { + mode = background_discovery; + } + break; + + case request_authorization_server_metadata: + if (details::request_authorization_server_metadata(model, authorization_state, authorization_service_error, load_ca_certificates, gate)) + { + // reterive client metadat from cache + const auto client_metadata = with_read_lock(authorization_state.mutex, [&] { return nmos::experimental::get_client_metadata(authorization_state); }); + + // is it not a scopeless client (where scopeless client doesn't access any protected APIs, i.e. doesn't require to register to Authorization server) + if (with_read_lock(model.mutex, [&] { return details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)).size(); })) + { + // is the client already registered to Authorization server, i.e. found in cache + if (!client_metadata.is_null()) + { + auto is_authorization_code_flow_next = [&] + { + auto lock = authorization_state.read_lock(); + + const auto& bearer_token = authorization_state.bearer_token; + return (!bearer_token.is_valid_access_token() || is_token_expired(bearer_token.access_token(), authorization_state.issuers, authorization_state.authorization_server_uri, gate)); + }; + + auto is_client_expired = [&] + { + // Time at which the client_secret will expire. If time is 0, it will never expire + // The time is represented as the number of seconds from 1970-01-01T0:0:0Z as measured in UTC + const auto expires_at = nmos::experimental::fields::client_secret_expires_at(client_metadata); + if (expires_at == 0) + { + return false; + } + auto now = std::chrono::system_clock::now(); + auto exp = std::chrono::system_clock::from_time_t(expires_at); + return (now > exp); + }; + + utility::string_t authorization_flow; + auto validate_openid_client = false; + with_read_lock(model.mutex, [&] + { + authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); + validate_openid_client = nmos::experimental::fields::validate_openid_client(model.settings); + }); + + // if using OpenID Connect Authorization server, update the cache client metadata, in case it has been changed (e.g. changed by the system admin) + if (validate_openid_client) + { + // if OpenID Connect Authorization server is used, client status can be obtained via the Client Configuration Endpoint + // "The Client Configuration Endpoint is an OAuth 2.0 Protected Resource that MAY be provisioned by the server for a + // specific Client to be able to view and update its registered information." + // see 3.2 of https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint + // registration_access_token + // OPTIONAL. Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the + // Client registration. + // registration_client_uri + // OPTIONAL. Location of the Client Configuration Endpoint where the Registration Access Token can be used to perform subsequent operations + // upon the resulting Client registration. + // Implementations MUST either return both a Client Configuration Endpoint and a Registration Access Token or neither of them. + if (client_metadata.has_string_field(nmos::experimental::fields::registration_access_token) && client_metadata.has_string_field(nmos::experimental::fields::registration_client_uri)) + { + // fetch client metadata from Authorization server in case it has been changed (e.g. changed by the system admin) + if (details::request_client_metadata_from_openid_connect(model, authorization_state, load_ca_certificates, save_authorization_client, gate)) + { + mode = (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : (is_authorization_code_flow_next() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch); + } + else + { + // remove client metadata from cache + auto lock = authorization_state.read_lock(); + nmos::experimental::erase_client_metadata(authorization_state); + + // client not known by the Authorization server, trigger client registration process + mode = client_registration; + } + } + else + { + // no registration_access_token and registration_client_uri found, treat it has connected with a non-OpenID Connect server + // start grant flow based on what been defined in the settings + // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration + mode = is_client_expired() ? client_registration : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : (is_authorization_code_flow_next() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); + } + } + else + { + // start grant flow based on what been defined in the settings + // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration + mode = is_client_expired() ? client_registration : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : (is_authorization_code_flow_next() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); + } + } + else + { + // client has not been registered to the Authorization server yet + mode = client_registration; + } + } + else + { + // scope-less client, not require to obtain access token + mode = authorization_operation; + } + } + else + { + // Should no further Authorization APIs be available or TTLs on advertised services expired, a re-query may be performed. + mode = rediscovery; + } + break; + + case client_registration: + // register to the Authorization server to obtain client_id and client_secret (they can be found inside the client metadata) + if (details::client_registration(model, authorization_state, load_ca_certificates, save_authorization_client, gate)) + { + // client registered + mode = with_read_lock(model.mutex, [&] + { + const auto& authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); + return (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : authorization_code_flow; + }); + } + else + { + // client registration failure, start authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + } + break; + + case authorization_code_flow: + if (details::authorization_code_flow(model, authorization_state, request_authorization_code, gate)) + { + mode = authorization_operation; + } + else + { + // authorization code flow failure, start authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + } + break; + + case authorization_operation: + // fetch public keys + // fetch access token in 1/2 token life time interval + details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, false, gate); + + // reaching here, there must be failure within the authorization operation, + // start authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + break; + + case authorization_operation_with_immediate_token_fetch: + // fetch public keys + // immediately fetch access token + details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, true, gate); + + // reaching here, there must be failure within the authorization operation, + // start authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + break; + + case background_discovery: + details::authorization_services_background_discovery(model, discovery, gate); + + if (details::has_discovered_authorization_services(model)) + { + mode = request_authorization_server_metadata; + } + } + } + } + + void authorization_token_issuer_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::authorization_behaviour)); + + details::authorization_token_issuer_thread(model, authorization_state, load_ca_certificates, gate); + } + + void details::authorization_token_issuer_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + enum + { + fetch_issuer_metadata, + fetch_issuer_pubkeys, + } mode = fetch_issuer_metadata; + + // continue until the server is being shut down + for (;;) + { + if (with_read_lock(model.mutex, [&] { return model.shutdown; })) break; + + switch (mode) + { + case fetch_issuer_metadata: + // fetch token issuer metadata + if (details::request_token_issuer_metadata(model, authorization_state, load_ca_certificates, gate)) + { + mode = fetch_issuer_pubkeys; + } + break; + + case fetch_issuer_pubkeys: + // fetch token issuer public keys + details::request_token_issuer_public_keys(model, authorization_state, load_ca_certificates, gate); + mode = fetch_issuer_metadata; + break; + } + } + } + + // service discovery + namespace details + { + static web::json::value make_service(const resolved_service& service) + { + using web::json::value; + + return web::json::value_of({ + { nmos::experimental::fields::ver, value::string(make_api_version(service.first.first)) }, + { nmos::fields::pri, service.first.second }, + { nmos::experimental::fields::uri, value::string(service.second.to_string()) } + }); + } + + static resolved_service parse_service(const web::json::value& data) + { + + return { + {parse_api_version(nmos::experimental::fields::ver(data)), nmos::fields::pri(data)}, + web::uri(nmos::experimental::fields::uri(data)) + }; + } + + // get the fallback authorization service from settings (if present) + resolved_service get_authorization_service(const nmos::settings& settings) + { + if (settings.has_field(nmos::experimental::fields::authorization_address)) + { + const auto api_selector = nmos::experimental::fields::authorization_selector(settings); + + return { { parse_api_version(nmos::experimental::fields::authorization_version(settings)), 0 }, + web::uri_builder() + .set_scheme(nmos::http_scheme(settings)) + .set_host(nmos::experimental::fields::authorization_address(settings)) + .set_port(nmos::experimental::fields::authorization_port(settings)) + .set_path(U("/.well-known/oauth-authorization-server")).append_path(!api_selector.empty() ? U("/") + api_selector : U("")) + .to_uri() }; + } + return {}; + } + + // query DNS Service Discovery for any Authorization API based on settings + bool discover_authorization_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token) + { + slog::log(gate, SLOG_FLF) << "Trying Authorization API discovery"; + + // lock to read settings, then unlock to wait for the discovery task to complete + auto authorization_services = with_read_lock(model.mutex, [&] + { + auto& settings = model.settings; + + if (nmos::service_priorities::no_priority != nmos::fields::authorization_highest_pri(settings)) + { + slog::log(gate, SLOG_FLF) << "Attempting discovery of a Authorization API in domain: " << nmos::get_domain(settings); + + return nmos::experimental::resolve_service_(discovery, nmos::service_types::authorization, settings, token); + } + else + { + return pplx::task_from_result(std::list{}); + } + }).get(); + + with_write_lock(model.mutex, [&] + { + if (!authorization_services.empty()) + { + slog::log(gate, SLOG_FLF) << "Discovered " << authorization_services.size() << " Authorization API(s)"; + } + else + { + slog::log(gate, SLOG_FLF) << "Did not discover a suitable Authorization API via DNS-SD"; + + auto fallback_authorization_service = get_authorization_service(model.settings); + if (!fallback_authorization_service.second.is_empty()) + { + authorization_services.push_back(fallback_authorization_service); + } + } + + if (!authorization_services.empty()) slog::log(gate, SLOG_FLF) << "Using the Authorization API(s):" << slog::log_manip([&](slog::log_statement& s) + { + for (auto& authorization_service : authorization_services) + { + s << '\n' << authorization_service.second.to_string(); + } + }); + + model.settings[nmos::experimental::fields::authorization_services] = web::json::value_from_elements(authorization_services | boost::adaptors::transformed([](const resolved_service& authorization_service) { return make_service(authorization_service); })); + + model.notify(); + }); + + return !authorization_services.empty(); + } + + bool empty_authorization_services(const nmos::settings& settings) + { + return web::json::empty(nmos::experimental::fields::authorization_services(settings)); + } + + bool has_discovered_authorization_services(const nmos::base_model& model) + { + return with_read_lock(model.mutex, [&] { return !empty_authorization_services(model.settings); }); + } + + // "The Node selects an Authorization API to use based on the priority" + resolved_service top_authorization_service(const nmos::settings& settings) + { + const auto value = web::json::front(nmos::experimental::fields::authorization_services(settings)); + return parse_service(value); + } + + // "If the chosen Authorization API does not respond correctly at any time, + // another Authorization API should be selected from the discovered list." + void pop_authorization_service(nmos::settings& settings) + { + web::json::pop_front(nmos::experimental::fields::authorization_services(settings)); + // "TTLs on advertised services" may have expired too, so should cache time-to-live values + // using DNSServiceQueryRecord instead of DNSServiceResolve? + } + } + + // service discovery operation + namespace details + { + void authorization_services_background_discovery(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Adopting background discovery of an Authorization API"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_services_discovered(false); + + // background tasks may read/write the above local state by reference + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task background_discovery = pplx::do_while([&] + { + // add a short delay since initial discovery or rediscovery must have only just failed + // (this also prevents a tight loop in the case that the underlying DNS-SD implementation is just refusing to co-operate + // though that would be better indicated by an exception from discover_authorization_services) + return pplx::complete_after(std::chrono::seconds(1), token).then([&] + { + return !discover_authorization_services(model, discovery, gate, token); + }); + }, token).then([&] + { + auto lock = model.write_lock(); // in order to update local state + + authorization_services_discovered = true; // since discovery must have succeeded + + model.notify(); + }); + + for (;;) + { + // wait for the thread to be interrupted because an Authorization API has been discovered + // or because the server is being shut down + condition.wait(lock, [&] { return shutdown || authorization_services_discovered; }); + if (shutdown || authorization_services_discovered) break; + } + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + background_discovery.wait(); + } + } + } +} diff --git a/Development/nmos/authorization_behaviour.h b/Development/nmos/authorization_behaviour.h new file mode 100644 index 000000000..fce1f72f5 --- /dev/null +++ b/Development/nmos/authorization_behaviour.h @@ -0,0 +1,50 @@ +#ifndef NMOS_AUTHORIZATION_BEHAVIOUR_H +#define NMOS_AUTHORIZATION_BEHAVIOUR_H + +#include +#include "cpprest/http_client.h" +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" +#include "nmos/mdns.h" +#include "nmos/settings.h" // just a forward declaration of nmos::settings + +namespace slog +{ + class base_gate; +} + +namespace mdns +{ + class service_discovery; +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + // uses the default DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, slog::base_gate& gate); + + // uses the specified DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate); + + // callbacks from this function are called with the model locked, and may read or write directly to the model and the authorization settings + void authorization_token_issuer_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + namespace details + { + // services functions which are used by authorization operation + bool empty_authorization_services(const nmos::settings& settings); + resolved_service top_authorization_service(const nmos::settings& settings); + void pop_authorization_service(nmos::settings& settings); + } + } +} + +#endif diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp new file mode 100644 index 000000000..30cc1b299 --- /dev/null +++ b/Development/nmos/authorization_handlers.cpp @@ -0,0 +1,267 @@ +#include "nmos/authorization_handlers.h" + +#include "cpprest/basic_utils.h" +#include "cpprest/json_validator.h" +#include "cpprest/response_type.h" +#include "nmos/api_utils.h" // for nmos::experimental::details::make_validate_authorization_handler +#include "nmos/authorization_state.h" +#include "nmos/is10_versions.h" +#include "nmos/json_schema.h" +#include "nmos/json_fields.h" +#include "nmos/slog.h" +#if defined(_WIN32) && !defined(__cplusplus_winrt) +#include +#include +#endif + +namespace nmos +{ + namespace experimental + { + namespace details + { + static const web::json::experimental::json_validator& auth_clients_schema_validator() + { + static const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(is10_versions::all | boost::adaptors::transformed(experimental::make_auth_clients_schema_uri)) + }; + return validator; + } + } + + // load the table of authorization server vs authorization client metadata from file + static web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate) + { + using web::json::value; + + utility::ifstream_t is(filename); + if (is.is_open()) + { + return value::parse(is); + } + return web::json::value::array(); + } + + // construct callback to load a table of authorization server uri vs authorization client metadata from file based on settings seed_id + // it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs) + load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate) + { + return [&]() + { + // obtain client metadata from the safe, permission-restricted, location in the non-volatile memory, e.g. a file + // Client metadata SHOULD consist of the client_id, client_secret, client_secret_expires_at, client_uri, grant_types, redirect_uris, response_types, scope, token_endpoint_auth_method + auto filename = nmos::experimental::fields::seed_id(settings) + U(".json"); + slog::log(gate, SLOG_FLF) << "Load authorization client from non-volatile memory: " << filename; + + // example of the authorization client file + // [ + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // ] + + try + { + const auto authorization_clients = load_authorization_clients_file(filename, gate); + + details::auth_clients_schema_validator().validate(authorization_clients, experimental::make_auth_clients_schema_uri(is10_versions::v1_0)); + + return authorization_clients; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to load authorization client from non-volatile memory: " << filename << ": " << e.what(); + + return web::json::value::array(); + } + }; + } + + // construct callback to save the authorization server uri vs authorization client metadata table to file, using seed_id for the filename + // it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs) + save_authorization_client_handler make_save_authorization_client_handler(const nmos::settings& settings, slog::base_gate& gate) + { + return [&](const web::json::value& authorization_client) + { + // Client metadata SHOULD be stored in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. + // Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials + const auto filename = nmos::experimental::fields::seed_id(settings) + U(".json"); + slog::log(gate, SLOG_FLF) << "Save authorization client to non-volatile memory: " << filename; + + // exmaple of authorization client JSON + // { + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "issuer" : "https://example.com", + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // } + + // load authorization_clients from file + web::json::value authorization_clients; + try + { + authorization_clients = load_authorization_clients_file(filename, gate); + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to load authorization client from non-volatile memory: " << filename << ": " << e.what(); + + authorization_clients = web::json::value::array(); + } + + // insert client to authorization_clients + bool inserted{ false }; + if (authorization_clients.as_array().size()) + { + for (auto& setting : authorization_clients.as_array()) + { + const auto& authorization_server_uri = setting.at(nmos::experimental::fields::authorization_server_uri); + if (authorization_server_uri == authorization_client.at(nmos::experimental::fields::authorization_server_uri)) + { + setting[nmos::experimental::fields::client_metadata] = authorization_client.at(nmos::experimental::fields::client_metadata); + inserted = true; + break; + } + } + } + if (!inserted) + { + web::json::push_back(authorization_clients, authorization_client); + } + + // save the updated authorization_clients to file + utility::ofstream_t os(filename, std::ios::out | std::ios::trunc); + if (os.is_open()) + { + os << authorization_clients.serialize(); + os.close(); + } + }; + } + + // construct callback to start the authorization code flow request on a browser + // it is required for OAuth client which is using the Authorization Code Flow to obtain the token + request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate) + { + return[&gate](const web::uri& authorization_code_uri) + { + slog::log(gate, SLOG_FLF) << "Open a browser to start the authorization code flow: " << authorization_code_uri.to_string(); + +#if defined(_WIN32) && !defined(__cplusplus_winrt) + ShellExecuteA(NULL, "open", utility::us2s(authorization_code_uri.to_string()).c_str(), NULL, NULL, SW_SHOWNORMAL); +#else + auto browser_cmd(U("xdg-open \"") + authorization_code_uri.to_string() + U("\"")); + if (0 > system(browser_cmd.c_str())) + { + slog::log(gate, SLOG_FLF) << "Faile to open a browser to start the authorization code flow"; + } +#endif + }; + } + + // construct callback to make OAuth 2.0 config + authorization_config_handler make_authorization_config_handler(const web::json::value& authorization_server_metadata, const web::json::value& client_metadata, slog::base_gate& gate) + { + return[&](const web::http::oauth2::experimental::oauth2_token& bearer_token) + { + slog::log(gate, SLOG_FLF) << "Make OAuth 2.0 config"; + + web::http::oauth2::experimental::oauth2_config config( + client_metadata.is_null() ? U("") : nmos::experimental::fields::client_id(client_metadata), + client_metadata.is_null() ? U("") : client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""), + authorization_server_metadata.is_null() ? U("") : nmos::experimental::fields::authorization_endpoint(authorization_server_metadata), + authorization_server_metadata.is_null() ? U("") : nmos::experimental::fields::token_endpoint(authorization_server_metadata), + client_metadata.is_null() ? U("") : client_metadata.has_array_field(nmos::experimental::fields::redirect_uris) && nmos::experimental::fields::redirect_uris(client_metadata).size() ? nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string() : U(""), + client_metadata.is_null() ? U("") : client_metadata.has_string_field(nmos::experimental::fields::scope) ? nmos::experimental::fields::scope(client_metadata) : U("")); + + if (!client_metadata.is_null()) + { + const auto& response_types = nmos::experimental::fields::response_types(client_metadata); + bool found_code = false; + bool found_token = false; + for (auto response_type : response_types) + { + if (web::http::oauth2::experimental::response_types::code.name == response_type.as_string()) { found_code = true; } + else if (web::http::oauth2::experimental::response_types::token.name == response_type.as_string()) { found_token = true; } + }; + config.set_bearer_auth(found_code || !found_token); + } + + config.set_token(bearer_token); + + return config; + }; + } + authorization_config_handler make_authorization_config_handler(const authorization_state& authorization_state, slog::base_gate& gate) + { + return[&](const web::http::oauth2::experimental::oauth2_token& /*bearer_token*/) + { + slog::log(gate, SLOG_FLF) << "Make OAuth 2.0 config using bearer_token cache"; + + auto lock = authorization_state.read_lock(); + + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); + const auto client_metadata = get_client_metadata(authorization_state); + + auto make_authorization_config = make_authorization_config_handler(authorization_server_metadata, client_metadata, gate); + return make_authorization_config(authorization_state.bearer_token); + }; + } + + validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, slog::base_gate& gate) + { + return[&](const nmos::experimental::scope& scope) + { + slog::log(gate, SLOG_FLF) << "Make authorization validation"; + + return nmos::experimental::details::make_validate_authorization_handler(model, authorization_state, scope, gate); + }; + } + + authorization_token_handler make_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) + { + return[&]() + { + slog::log(gate, SLOG_FLF) << "Retrieve bearer token from cache"; + + auto lock = authorization_state.read_lock(); + return authorization_state.bearer_token; + }; + } + } +} diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h new file mode 100644 index 000000000..4d238c6c7 --- /dev/null +++ b/Development/nmos/authorization_handlers.h @@ -0,0 +1,135 @@ +#ifndef NMOS_AUTHORIZATION_HANDLERS_H +#define NMOS_AUTHORIZATION_HANDLERS_H + +#include +#include "cpprest/api_router.h" +#include "cpprest/oauth2.h" +#include "nmos/scope.h" +#include "nmos/settings.h" + +namespace slog +{ + class base_gate; +} + +namespace web +{ + class uri; + + namespace json + { + class value; + } +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + namespace fields + { + // authorization_server_uri: the uri of the authorization server, where the client is registered + const web::json::field_as_string_or authorization_server_uri{ U("authorization_server_uri"), U("") }; + + // client_metadata: the registered client metadata + // already defined in nmos/json_fields.h + //const web::json::field_as_value client_metadata{ U("client_metadata") }; + } + + // callback to supply a list of authorization clients + // callbacks from this function are called with the model locked, and may read or write directly to the model + // this callback should not throw exceptions + // example JSON of the authorization client list + // [ + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // ] + typedef std::function load_authorization_clients_handler; + + // callback after authorization client has registered + // callbacks from this function are called with the model locked, and may read or write directly to the model + // this callback should not throw exceptions + // example JSON of the client_metadata + // { + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "issuer" : "https://example.com", + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // } + typedef std::function save_authorization_client_handler; + + // callback on requesting to start off the authorization code grant flow + // callbacks from this function are called with the model locked, and may read or write directly to the model + // this callback should not throw exceptions + typedef std::function request_authorization_code_handler; + + // construct callback to load a table of authorization server uri vs authorization clients metadata from file based on settings seed_id + load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to save authorization client metadata to file based on seed_id from settings + save_authorization_client_handler make_save_authorization_client_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to start the authorization code flow request on a browser + request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate); + + // callback to return OAuth 2.0 authorization config + // this callback is executed while constructing http_client_config + // this callback should not throw exceptions + typedef std::function authorization_config_handler; + // construct callback to make OAuth 2.0 config + authorization_config_handler make_authorization_config_handler(const web::json::value& authorization_server_metadata, const web::json::value& client_metadata, slog::base_gate& gate); + authorization_config_handler make_authorization_config_handler(const authorization_state& authorization_state, slog::base_gate& gate); + + // callback to return the OAuth 2.0 validation route handler + // this callback is executed at the beginning while walking the supported API routes + typedef std::function validate_authorization_handler; + // construct callback to validate authorization + validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, slog::base_gate& gate); + + // callback to return OAuth 2.0 authorization bearer token + // this callback is execute while create http_client + typedef std::function authorization_token_handler; + // construct callback to retrieve authorization bearer token + // this callback should not throw exceptions + authorization_token_handler make_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp new file mode 100644 index 000000000..5d25c8014 --- /dev/null +++ b/Development/nmos/authorization_operation.cpp @@ -0,0 +1,1900 @@ +#include "nmos/authorization_operation.h" + +#include +#include "cpprest/code_challenge_method.h" +#include "cpprest/json_validator.h" +#include "cpprest/response_type.h" +#include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_scopes.h" +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/client_utils.h" +#include "nmos/is10_versions.h" +#include "nmos/json_schema.h" +#include "nmos/jwt_generator.h" +#include "nmos/jwk_utils.h" +#include "nmos/model.h" +#include "nmos/random.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + // authorization operation + namespace details + { + static const web::json::experimental::json_validator& authapi_validator() + { + static const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(boost::join(boost::join(boost::join(boost::join( + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_auth_metadata_schema_uri), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_jwks_response_schema_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_register_client_response_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_response_schema_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_schema_schema_uri))) + }; + return validator; + } + + // build the scope string with given list of scopes + utility::string_t make_scope(const std::set& scopes_) + { + utility::string_t scopes; + for (const auto& scope : scopes_) + { + if (!scopes.empty()) { scopes += U(" "); } + scopes += scope.name; + } + return scopes; + } + + // build grant array with given list of grants + web::json::value make_grant_types(const std::set& grants) + { + auto grant_types = web::json::value::array(); + for (const auto& grant : grants) + { + web::json::push_back(grant_types, grant.name); + } + return grant_types; + } + + // generate SHA256 with the given string + std::vector sha256(const std::string& text) + { + uint8_t hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX ctx; + if (SHA256_Init(&ctx) && SHA256_Update(&ctx, text.c_str(), text.size()) && SHA256_Final(hash, &ctx)) + { + return{ hash, hash + SHA256_DIGEST_LENGTH }; + } + return{}; + } + + // use the authorization URI on a web browser to start the authorization code grant workflow + web::uri make_authorization_code_uri(const web::uri& authorization_endpoint, const utility::string_t& client_id, const web::uri& redirect_uri, const web::http::oauth2::experimental::response_type& response_type, const std::set& scopes, const web::json::array& code_challenge_methods_supported, utility::string_t& state, utility::string_t& code_verifier) + { + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub(authorization_endpoint); + ub.append_query(oauth2_strings::client_id, client_id); + ub.append_query(oauth2_strings::redirect_uri, redirect_uri.to_string()); + ub.append_query(oauth2_strings::response_type, response_type.name); + + // using PKCE? + if (code_challenge_methods_supported.size()) + { + const auto found = std::find_if(code_challenge_methods_supported.begin(), code_challenge_methods_supported.end(), [&](const web::json::value& code_challenge_method) + { + return web::http::oauth2::experimental::code_challenge_methods::S256.name == code_challenge_method.as_string(); + }); + + const auto code_challenge_method = (code_challenge_methods_supported.end() != found) ? web::http::oauth2::experimental::code_challenge_methods::S256 : web::http::oauth2::experimental::code_challenge_methods::plain; + + // code_verifier = high-entropy cryptographic random STRING using the + // unreserved characters[A - Z] / [a - z] / [0 - 9] / "-" / "." / "_" / "~" + // from Section 2.3 of[RFC3986], with a minimum length of 43 characters + // and a maximum length of 128 characters + // see https://tools.ietf.org/html/rfc7636#section-4.1 + { + utility::nonce_generator generator(128); + code_verifier = generator.generate(); + } + + // creates code challenge from code verifier + // see https://tools.ietf.org/html/rfc7636#section-4.2 + utility::string_t code_challenge{}; + if (web::http::oauth2::experimental::code_challenge_methods::plain == code_challenge_method) + { + code_challenge = code_verifier; + } + else + { + const auto sha256 = nmos::experimental::details::sha256(utility::us2s(code_verifier)); + code_challenge = utility::conversions::to_base64url(sha256); + } + ub.append_query(U("code_challenge"), code_challenge); + ub.append_query(U("code_challenge_method"), code_challenge_method.name); + } + + utility::nonce_generator generator; + state = generator.generate(); + ub.append_query(oauth2_strings::state, state); + + if (scopes.size()) + { + ub.append_query(oauth2_strings::scope, make_scope(scopes)); + } + + return ub.to_uri(); + } + + // it is used to strip the trailing dot of the FQDN if it is presented + utility::string_t strip_trailing_dot(const utility::string_t& host_) + { + auto host = host_; + if (!host.empty() && U('.') == host.back()) + { + host.pop_back(); + } + return host; + } + + // construct the redirect URI from settings + // format of the authorization_redirect_uri "://:/x-authorization/callback/" + web::uri make_authorization_redirect_uri(const nmos::settings& settings) + { + return web::uri_builder() + .set_scheme(web::http_scheme(nmos::experimental::fields::client_secure(settings))) + .set_host(nmos::experimental::fields::no_trailing_dot_for_authorization_callback_uri(settings) ? strip_trailing_dot(get_host(settings)) : get_host(settings)) + .set_port(nmos::experimental::fields::authorization_redirect_port(settings)) + .set_path(U("/x-authorization/callback")) + .to_uri(); + } + + // construct the jwks URI from settings + // format of the jwks_uri "://:/x-authorization/jwks/" + web::uri make_jwks_uri(const nmos::settings& settings) + { + return web::uri_builder() + .set_scheme(web::http_scheme(nmos::experimental::fields::client_secure(settings))) + .set_host(nmos::experimental::fields::no_trailing_dot_for_authorization_callback_uri(settings) ? strip_trailing_dot(get_host(settings)) : get_host(settings)) + .set_port(nmos::experimental::fields::jwks_uri_port(settings)) + .set_path(U("/x-authorization/jwks")) + .to_uri(); + } + + // construct the authorization server URI using the given URI authority + // format of the authorization_service_uri "://:/.well-known/oauth-authorization-server[/]" + web::uri make_authorization_service_uri(const web::uri& uri, const utility::string_t& api_selector = {}) + { + return web::uri_builder(uri.authority()).set_path(U("/.well-known/oauth-authorization-server")).append_path(!api_selector.empty() ? U("/") + api_selector : U("")).to_uri(); + } + + // construct authorization client config based on settings + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) + { + auto config = nmos::make_http_client_config(settings, load_ca_certificates, make_authorization_config, bearer_token, gate); + config.set_timeout(std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings))); + + return config; + } + + struct authorization_exception {}; + + // parse the given json to obtain access token + // this function is based on the oauth2_config::_parse_token_from_json(const json::value& token_json) from cpprestsdk's oauth2.cpp + web::http::oauth2::experimental::oauth2_token parse_token_from_json(const web::json::value& token_json) + { + using web::http::oauth2::details::oauth2_strings; + using web::http::oauth2::experimental::oauth2_token; + using web::http::oauth2::experimental::oauth2_exception; + + oauth2_token result; + + if (token_json.has_string_field(oauth2_strings::access_token)) + { + result.set_access_token(token_json.at(oauth2_strings::access_token).as_string()); + } + else + { +#if defined (NDEBUG) + throw oauth2_exception(U("response json contains no 'access_token'")); +#else + throw oauth2_exception(U("response json contains no 'access_token': ") + token_json.serialize()); +#endif + } + + if (token_json.has_string_field(oauth2_strings::token_type)) + { + result.set_token_type(token_json.at(oauth2_strings::token_type).as_string()); + } + else + { + // Some services don't return 'token_type' while it's required by OAuth 2.0 spec: + // http://tools.ietf.org/html/rfc6749#section-5.1 + // As workaround we act as if 'token_type=bearer' was received. + result.set_token_type(oauth2_strings::bearer); + } + if (!utility::details::str_iequal(result.token_type(), oauth2_strings::bearer)) + { +#if defined (NDEBUG) + throw oauth2_exception(U("only bearer tokens are currently supported")); +#else + throw oauth2_exception(U("only bearer tokens are currently supported: ") + token_json.serialize()); +#endif + } + + if (token_json.has_string_field(oauth2_strings::refresh_token)) + { + result.set_refresh_token(token_json.at(oauth2_strings::refresh_token).as_string()); + } + else + { + // Do nothing. Preserves the old refresh token + } + + if (token_json.has_field(oauth2_strings::expires_in)) + { + const auto& json_expires_in_val = token_json.at(oauth2_strings::expires_in); + + if (json_expires_in_val.is_number()) + { + result.set_expires_in(json_expires_in_val.as_number().to_int64()); + } + else + { + // Handle the case of a number as a JSON "string" + int64_t expires; + utility::istringstream_t iss(json_expires_in_val.as_string()); + iss.exceptions(std::ios::badbit | std::ios::failbit); + iss >> expires; + result.set_expires_in(expires); + } + } + else + { + result.set_expires_in(oauth2_token::undefined_expiration); + } + + if (token_json.has_string_field(oauth2_strings::scope)) + { + // The authorization server may return different scope from the one requested + // This however doesn't necessarily mean the token authorization scope is different + // See: http://tools.ietf.org/html/rfc6749#section-3.3 + result.set_scope(token_json.at(oauth2_strings::scope).as_string()); + } + + return result; + } + + // make an asynchronously GET request on the Authorization API to fetch authorization server metadata + pplx::task request_authorization_server_metadata(web::http::client::http_client client, const std::set& scopes, const std::set& grants, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting authorization server metadata"; + + using namespace web::http; + + // ://:/.well-known/oauth-authorization-server[/] + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#authorization-server-metadata-endpoint + return nmos::api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + namespace response_types = web::http::oauth2::experimental::response_types; + namespace grant_types = web::http::oauth2::experimental::grant_types; + + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + if (response.body()) + { + return response.extract_json().then([=, &gate](web::json::value metadata) + { + // validate server metadata + authapi_validator().validate(metadata, experimental::make_authapi_auth_metadata_schema_uri(version)); // may throw json_exception + + // hmm, verify Authorization server meeting the minimum client requirement + + // is the required response_types supported by Authorization server (response_types_supported) + std::set response_types = { response_types::code }; + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return grant_types::implicit == grant; })) + { + response_types.insert(response_types::token); + } + if (response_types.size()) + { + const auto supported = std::all_of(response_types.begin(), response_types.end(), [&](const web::http::oauth2::experimental::response_type& response_type) + { + const auto& response_types_supported = nmos::experimental::fields::response_types_supported(metadata); + const auto found = std::find_if(response_types_supported.begin(), response_types_supported.end(), [&response_type](const web::json::value& response_type_) { return response_type_.as_string() == response_type.name; }); + return response_types_supported.end() != found; + }); + if (!supported) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting all the required response types"; + throw authorization_exception(); + } + } + + // scopes_supported is optional + // is required scopes supported by Authorization server (scopes_supported) + if (scopes.size() && metadata.has_array_field(nmos::experimental::fields::scopes_supported)) + { + const auto supported = std::all_of(scopes.begin(), scopes.end(), [&](const nmos::experimental::scope& scope) + { + const auto& scopes_supported = nmos::experimental::fields::scopes_supported(metadata); + const auto found = std::find_if(scopes_supported.begin(), scopes_supported.end(), [&scope](const web::json::value& scope_) { return scope_.as_string() == scope.name; }); + return scopes_supported.end() != found; + }); + if (!supported) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting all the required scopes: " << [&scopes]() { std::stringstream ss; for (auto scope : scopes) ss << utility::us2s(scope.name) << " "; return ss.str(); }(); + throw authorization_exception(); + } + } + + // grant_types_supported is optional + // is required grants supported by Authorization server (grant_types_supported) + if (grants.size() && metadata.has_array_field(nmos::experimental::fields::grant_types_supported)) + { + const auto supported = std::all_of(grants.begin(), grants.end(), [&](const web::http::oauth2::experimental::grant_type& grant) + { + const auto& grants_supported = nmos::experimental::fields::grant_types_supported(metadata); + const auto found = std::find_if(grants_supported.begin(), grants_supported.end(), [&grant](const web::json::value& grant_) { return grant_.as_string() == grant.name; }); + return grants_supported.end() != found; + }); + if (!supported) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting all the required grants: " << [&grants]() { std::stringstream ss; for (auto grant : grants) ss << utility::us2s(grant.name) << " "; return ss.str(); }(); + throw authorization_exception(); + } + } + + // token_endpoint_auth_methods_supported is optional + // is required token_endpoint_auth_method supported by Authorization server (token_endpoint_auth_methods_supported) + if (metadata.has_array_field(nmos::experimental::fields::token_endpoint_auth_methods_supported)) + { + const auto& supported = nmos::experimental::fields::token_endpoint_auth_methods_supported(metadata); + const auto found = std::find_if(supported.begin(), supported.end(), [&token_endpoint_auth_method](const web::json::value& token_endpoint_auth_method_) { return token_endpoint_auth_method_.as_string() == token_endpoint_auth_method.name; }); + if (supported.end() == found) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting the required token_endpoint_auth_method:" << token_endpoint_auth_method.name; + throw authorization_exception(); + } + } + + slog::log(gate, SLOG_FLF) << "Received authorization server metadata: " << utility::us2s(metadata.serialize()); + return metadata; + }, token); + } + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: no response body"; + } + else + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: " << response.status_code() << " " << response.reason_phrase(); + } + throw authorization_exception(); + + }, token); + } + + // make an asynchronously POST request on the Authorization API to register a client + // see https://tools.ietf.org/html/rfc6749#section-2 + // see https://tools.ietf.org/html/rfc7591#section-3.1 + // e.g. curl -X POST "https://authorization.server.example.com/register" -H "Content-Type: application/json" -d "{\"redirect_uris\": [\"https://client.example.com/callback/\"],\"client_name\": \"My Example Client\",\"client_uri\": \"https://client.example.com/details.html\",\"token_endpoint_auth_method\": \"client_secret_basic\",\"response_types\": [\"code\",\"token\"],\"scope\": \"registration query node connection\",\"grant_types\": [\"authorization_code\",\"refresh_token\",\"client_credentials\"],\"token_endpoint_auth_method\": \"client_secret_basic\"}" + pplx::task request_client_registration(web::http::client::http_client client, const utility::string_t& client_name, const std::vector& redirect_uris, const web::uri& client_uri, const std::set& response_types, const std::set& scopes, const std::set& grants, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const web::json::value& jwk, const web::uri& jwks_uri, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting authorization client registration"; + + using namespace web; + using namespace web::http; + using web::json::value; + using web::json::value_of; + + const auto make_uris = [](const std::vector& uris) + { + auto result = value::array(); + for (const auto& uri : uris) { web::json::push_back(result, uri.to_string()); } + return result; + }; + + const auto make_response_types = [](const std::set& response_types) + { + auto result = value::array(); + for (const auto& response_type : response_types) { web::json::push_back(result, response_type.name); } + return result; + }; + + const auto make_scope = [](const std::set& scopes) + { + std::ostringstream os; + int idx{ 0 }; + for (const auto& scope : scopes) + { + if (idx++) { os << " "; } + os << utility::us2s(scope.name); + } + return value(utility::s2us(os.str())); + }; + + const auto make_grant_type = [](const std::set& grants) + { + auto result = value::array(); + for (const auto& grant : grants) { web::json::push_back(result, grant.name); } + return result; + }; + + // required + auto metadata = value_of({ + { nmos::experimental::fields::client_name, client_name } + }); + + // optional + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return web::http::oauth2::experimental::grant_types::authorization_code == grant; })) + { + metadata[nmos::experimental::fields::redirect_uris] = make_uris(redirect_uris); + } + if (!client_uri.is_empty()) + { + metadata[nmos::experimental::fields::client_uri] = value::string(client_uri.to_string()); + } + if (response_types.size()) + { + metadata[nmos::experimental::fields::response_types] = make_response_types(response_types); + } + if (scopes.size()) + { + metadata[nmos::experimental::fields::scope] = make_scope(scopes); + } + if (grants.size()) + { + metadata[nmos::experimental::fields::grant_types] = make_grant_type(grants); + } + + metadata[nmos::experimental::fields::token_endpoint_auth_method] = value::string(token_endpoint_auth_method.name); + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + if (!jwks_uri.is_empty()) + { + metadata[nmos::experimental::fields::jwks_uri] = value::string(jwks_uri.to_string()); + } + else + { + metadata[nmos::experimental::fields::jwks] = value_of({ + { nmos::experimental::fields::keys, value_of({ jwk }) } + }); + } + } + + slog::log(gate, SLOG_FLF) << "Request to register client metadata: " << utility::us2s(metadata.serialize()); + + return nmos::api_request(client, methods::POST, {}, metadata, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (response.body()) + { + return response.extract_json().then([=, &gate](web::json::value client_metadata) + { + if (status_codes::Created == response.status_code()) + { + slog::log(gate, SLOG_FLF) << "Registered client metadata: " << utility::us2s(client_metadata.serialize()); + + // validate client metadata + authapi_validator().validate(client_metadata, experimental::make_authapi_register_client_response_uri(version)); // may throw json_exception + + return client_metadata; + } + else + { + slog::log(gate, SLOG_FLF) << "Request client registration error: " << response.status_code() << " " << response.reason_phrase() << " " << utility::us2s(client_metadata.serialize()); + throw authorization_exception(); + } + }, token); + } + slog::log(gate, SLOG_FLF) << "Request client registration error: " << response.status_code() << " " << response.reason_phrase(); + throw authorization_exception(); + + }, token); + } + + // make an asynchronously GET request on the Authorization API to fetch the authorization JSON Web Keys (public keys) + pplx::task request_jwks(web::http::client::http_client client, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting authorization jwks"; + + using namespace web::http; + using oauth2::experimental::oauth2_exception; + + return nmos::api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + if (response.body()) + { + return nmos::details::extract_json(response, gate).then([version, &gate](web::json::value body) + { + // validate jwks JSON + authapi_validator().validate(body, experimental::make_authapi_jwks_response_schema_uri(version)); // may throw json_exception + + // MUST have a "keys" member! + // see https://tools.ietf.org/html/rfc7517#section-5 + if (!body.has_array_field(U("keys"))) throw web::http::http_exception(U("jwks contains no 'keys': ") + body.serialize()); + + const auto jwks = body.at(U("keys")); + + jwks.as_array().size() ? slog::log(gate, SLOG_FLF) << "Received authorization jwks: " << utility::us2s(jwks.serialize()) : + slog::log(gate, SLOG_FLF) << "Request authorization jwks: no jwk"; + + return jwks; + + }, token); + } + else + { + slog::log(gate, SLOG_FLF) << "Request authorization jwks error: no response body"; + } + } + else + { + slog::log(gate, SLOG_FLF) << "Request authorization jwks error: " << response.status_code() << " " << response.reason_phrase(); + } + throw authorization_exception(); + + }, token); + } + + // make an asynchronously GET request on the OpenID Connect Authorization API to fetch the client metdadata + pplx::task request_client_metadata_from_openid_connect(web::http::client::http_client client, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata"; + + using namespace web::http; + + return api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (response.body()) + { + return nmos::details::extract_json(response, gate).then([=, &gate](web::json::value body) + { + if (status_codes::OK == response.status_code()) + { + slog::log(gate, SLOG_FLF) << "Received OpenID Connect client metadata: " << utility::us2s(body.serialize()); + + // validate client metadata JSON + authapi_validator().validate(body, experimental::make_authapi_register_client_response_uri(version)); // may throw json_exception + + return body; + } + else + { + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata error: " << response.status_code() << " " << response.reason_phrase() << " " << utility::us2s(body.serialize()); + throw authorization_exception(); + } + }); + } + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata error: no response json: no client metadata"; + throw authorization_exception(); + + }, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token, + // this is a helper function which is used by the request_token_from_client_credentials and request_token_from_refresh_token + // see https://medium.com/@software_factotum/pkce-public-clients-and-refresh-token-d1faa4ef6965#:~:text=Refresh%20Token%20are%20credentials%20that,application%20needs%20additional%20access%20tokens.&text=Authorization%20Server%20may%20issue%20a,Client%20it%20was%20issued%20to. + pplx::task request_token(web::http::client::http_client client, const nmos::api_version& version, web::uri_builder& request_body_ub, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token"; + + using namespace web::http; + using oauth2::details::oauth2_strings; + using oauth2::experimental::oauth2_exception; + using oauth2::experimental::oauth2_token; + using web::http::details::mime_types; + + if (!scope.empty()) + { + request_body_ub.append_query(oauth2_strings::scope, uri::encode_data_string(scope), false); + } + + http_request req(methods::POST); + + if (client_secret.empty()) + { + if (!client_id.empty()) + { + // for Public Client or using private_key_jwt just append the client_id to query + request_body_ub.append_query(oauth2_strings::client_id, client_id, false); + } + } + else + { + // for Confidential Client and not using private_key_jwt + // Build HTTP Basic authorization header with 'client_id' and 'client_secret' + const std::string creds_utf8(utility::conversions::to_utf8string(uri::encode_data_string(client_id) + U(":") + uri::encode_data_string(client_secret))); + req.headers().add(header_names::authorization, U("Basic ") + utility::conversions::to_base64(std::vector(creds_utf8.begin(), creds_utf8.end()))); + } + + req.set_body(request_body_ub.query(), mime_types::application_x_www_form_urlencoded); + + return nmos::api_request(client, req, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (response.body()) + { + return nmos::details::extract_json(response, gate).then([=, &gate](web::json::value body) + { + if (status_codes::OK == response.status_code()) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Received bearer token"; +#else + slog::log(gate, SLOG_FLF) << "Received bearer token: " << utility::us2s(body.serialize()); +#endif + // validate bearer token JSON + authapi_validator().validate(body, experimental::make_authapi_token_response_schema_uri(version)); // may throw json_exception + + return parse_token_from_json(body); // may throw oauth2_exception + } + else + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: " << response.status_code() << " " << response.reason_phrase(); +#else + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: " << response.status_code() << " " << response.reason_phrase() << " " << utility::us2s(body.serialize()); +#endif + throw authorization_exception(); + } + }); + } + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: no response json: no bearer token"; + throw authorization_exception(); + + }, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using client_credentials grant + pplx::task request_token_from_client_credentials(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant"; + + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, U("client_credentials"), false); + + return request_token(client, version, ub, client_id, client_secret, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using client_credentials grant with private_key_jwt for client authentication + pplx::task request_token_from_client_credentials_using_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant with private_key_jwt"; + + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, U("client_credentials"), false); + + // use private_key_jwt client authentication + // see https://tools.ietf.org/html/rfc7523#section-2.2 + ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); + ub.append_query(U("client_assertion"), client_assertion, false); + + return request_token(client, version, ub, client_id, {}, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with code_verifier: " << utility::us2s(code_verifier); + + using namespace web::http; + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, oauth2_strings::authorization_code, false); + ub.append_query(oauth2_strings::code, web::uri::encode_data_string(code), false); + ub.append_query(oauth2_strings::redirect_uri, web::uri::encode_data_string(redirect_uri), false); + ub.append_query(U("code_verifier"), code_verifier, false); + + return request_token(client, version, ub, client_id, client_secret, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication + pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, const utility::string_t& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with " << utility::us2s(token_endpoint_auth_method) << " and code_verifier: "<< utility::us2s(code_verifier) << " and client_assertion: " << utility::us2s(client_assertion); + + using namespace web::http; + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, oauth2_strings::authorization_code, false); + ub.append_query(oauth2_strings::code, web::uri::encode_data_string(code), false); + ub.append_query(oauth2_strings::redirect_uri, web::uri::encode_data_string(redirect_uri), false); + ub.append_query(U("code_verifier"), code_verifier, false); + + // use private_key_jwt client authentication + // see https://tools.ietf.org/html/rfc7523#section-2.2 + ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); + ub.append_query(U("client_assertion"), client_assertion, false); + + return request_token(client, version, ub, client_id, {}, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant + pplx::task request_token_from_refresh_token(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& refresh_token, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant"; + + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); + ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + + return request_token(client, version, ub, client_id, client_secret, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant with private_key_jwt for client authentication + pplx::task request_token_from_refresh_token_using_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& refresh_token, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant with private_key_jwt"; + + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); + ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + + // use private_key_jwt client authentication + // see https://tools.ietf.org/html/rfc7523#section-2.2 + ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); + ub.append_query(U("client_assertion"), client_assertion, false); + + return request_token(client, version, ub, client_id, {}, scope, gate, token); + } + + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + // this function is based on the oauth2_config::token_from_redirected_uri + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token) + { + using web::http::oauth2::experimental::oauth2_exception; + using web::http::oauth2::details::oauth2_strings; + namespace response_types = web::http::oauth2::experimental::response_types; + + // authorization code grant + // redirected_uri: /x-authorization/callback/?state=&code= + // implicit grant + // redirected_uri: /x-authorization/callback/#state=&code= or + // redirected_uri: /x-authorization/callback/#state=&access_token= + auto query = web::uri::split_query(response_type == response_types::token.name ? redirected_uri.fragment() : redirected_uri.query()); + + auto state_param = query.find(oauth2_strings::state); + if (state_param == query.end()) + { + throw oauth2_exception(U("parameter 'state' missing from redirected URI")); + } + + if (state != state_param->second) + { + utility::string_t err(U("redirected URI parameter 'state'='")); + err += state_param->second; + err += U("' does not match excepted state='"); + err += state; + err += U("'"); + throw oauth2_exception(std::move(err)); + } + + auto code_param = query.find(oauth2_strings::code); + if (code_param != query.end()) + { + return request_token_from_authorization_code(client, version, client_id, client_secret, scope, code_param->second, redirect_uri, code_verifier, gate, token); + } + + // NOTE: The redirected URI contains access token only in the implicit grant + // The implicit grant never passes a refresh token + auto token_param = query.find(oauth2_strings::access_token); + if (token_param == query.end()) + { + throw oauth2_exception(U("either 'code' or 'access_token' parameter must be in the redirected URI")); + } + + return pplx::task_from_result(web::http::oauth2::experimental::oauth2_token(token_param->second)); + } + + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication + // this function is based on the oauth2_config::token_from_redirected_uri + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const utility::string_t& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token) + { + using web::http::oauth2::experimental::oauth2_exception; + using web::http::oauth2::details::oauth2_strings; + namespace response_types = web::http::oauth2::experimental::response_types; + + // authorization code grant + // redirected_uri: /auth/callback/?state=&code= + // implicit grant + // redirected_uri: /auth/callback/#state=&code= or + // redirected_uri: /auth/callback/#state=&access_token= + auto query = web::uri::split_query(response_type == response_types::token.name ? redirected_uri.fragment() : redirected_uri.query()); + + auto state_param = query.find(oauth2_strings::state); + if (state_param == query.end()) + { + throw oauth2_exception(U("parameter 'state' missing from redirected URI")); + } + + if (state != state_param->second) + { + utility::string_t err(U("redirected URI parameter 'state'='")); + err += state_param->second; + err += U("' does not match excepted state='"); + err += state; + err += U("'"); + throw oauth2_exception(std::move(err)); + } + + auto code_param = query.find(oauth2_strings::code); + if (code_param != query.end()) + { + return request_token_from_authorization_code(client, version, client_id, scope, code_param->second, redirect_uri, code_verifier, token_endpoint_auth_method, client_assertion, gate, token); + } + + // NOTE: The redirected URI contains access token only in the implicit grant + // The implicit grant never passes a refresh token + auto token_param = query.find(oauth2_strings::access_token); + if (token_param == query.end()) + { + throw oauth2_exception(U("either 'code' or 'access_token' parameter must be in the redirected URI")); + } + + return pplx::task_from_result(web::http::oauth2::experimental::oauth2_token(token_param->second)); + } + + struct token_shared_state + { + web::http::oauth2::experimental::grant_type grant_type; + web::http::oauth2::experimental::oauth2_token bearer_token; + std::unique_ptr client; + nmos::api_version version; // issuer version + load_rsa_private_keys_handler load_rsa_private_keys; + bool immediate; // true = do an immediate fetch; false = loop based on time interval + + explicit token_shared_state(web::http::oauth2::experimental::grant_type grant_type, web::http::oauth2::experimental::oauth2_token bearer_token, web::http::client::http_client client, nmos::api_version version, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate) + : grant_type(std::move(grant_type)) + , bearer_token(std::move(bearer_token)) + , client(std::unique_ptr(new web::http::client::http_client(client))) + , version(std::move(version)) + , load_rsa_private_keys(std::move(load_rsa_private_keys)) + , immediate(immediate) {} + }; + + // task to continous fetching bearer token in a time interval + pplx::task request_token(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, token_shared_state& token_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + const auto access_token_refresh_interval = nmos::experimental::fields::access_token_refresh_interval(model.settings); + + web::json::value authorization_server_metadata{}; + web::json::value client_metadata{}; + with_read_lock(authorization_state.mutex, [&] + { + authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); + client_metadata = nmos::experimental::get_client_metadata(authorization_state); + }); + + const auto client_id = nmos::experimental::fields::client_id(client_metadata); + const auto client_secret = client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""); + const auto scope = nmos::experimental::fields::scope(client_metadata); + const auto token_endpoint_auth_method = client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method) ? nmos::experimental::fields::token_endpoint_auth_method(client_metadata) : web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name; + const auto token_endpoint = nmos::experimental::fields::token_endpoint(authorization_server_metadata); + const auto client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(model.settings)); + + // start a background task for continuous fetching bearer token in a time interval + return pplx::do_while([=, &model, &authorization_state, &token_state, &gate] + { + auto fetch_interval = std::chrono::seconds(0); + if (!token_state.immediate && token_state.bearer_token.is_valid_access_token()) + { + // RECOMMENDED to attempt a refresh at least 15 seconds before expiry (i.e the half-life of the shortest-lived token possible) + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#refreshing-a-token + fetch_interval = access_token_refresh_interval < 0 ? std::chrono::seconds(token_state.bearer_token.expires_in() / 2) : std::chrono::seconds(access_token_refresh_interval); + } + token_state.immediate = false; + + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token for about " << fetch_interval.count() << " seconds"; + + auto fetch_time = std::chrono::steady_clock::now(); + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &model , &token_state, &gate]() + { + // create client assertion for private key jwt + utility::string_t client_assertion; + with_read_lock(model.mutex, [&] + { + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + { + // get RSA private key from list + if (!token_state.load_rsa_private_keys) + { + throw web::http::oauth2::experimental::oauth2_exception(U("missing RSA private key loader to extract RSA private key")); + } + auto rsa_private_key = details::found_rsa_key(token_state.load_rsa_private_keys()); + if (rsa_private_key.empty()) + { + throw web::http::oauth2::experimental::oauth2_exception(U("no RSA key to create client assertion")); + } + client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key); + } + }); + + if (web::http::oauth2::experimental::grant_types::authorization_code == token_state.grant_type) + { + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + { + return request_token_from_refresh_token_using_private_key_jwt(*token_state.client, token_state.version, client_id, scope, token_state.bearer_token.refresh_token(), client_assertion, gate, token); + } + else + { + return request_token_from_refresh_token(*token_state.client, token_state.version, client_id, client_secret, scope, token_state.bearer_token.refresh_token(), gate, token); + } + } + else if (web::http::oauth2::experimental::grant_types::client_credentials == token_state.grant_type) + { + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + { + return request_token_from_client_credentials_using_private_key_jwt(*token_state.client, token_state.version, client_id, scope, client_assertion, gate, token); + } + else + { + return request_token_from_client_credentials(*token_state.client, token_state.version, client_id, client_secret, scope, gate, token); + } + } + else + { + throw web::http::oauth2::experimental::oauth2_exception(U("Unsupported grant: ") + token_state.grant_type.name); + } + + }).then([=, &authorization_state, &token_state, &gate](web::http::oauth2::experimental::oauth2_token bearer_token) + { + token_state.bearer_token = bearer_token; + + // update token in authorization settings + auto lock = authorization_state.write_lock(); + authorization_state.bearer_token = token_state.bearer_token; + slog::log(gate, SLOG_FLF) << "'" << utility::us2s(scope) << "' bearer token updated"; + + return true; + }); + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request JSON error: " << e.what(); + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request OAuth 2.0 error: " << e.what(); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request error: " << e.what(); + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request error"; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request unexpected unknown exception"; + } + + // reaching here, there must be something has gone wrong with the Authorization Server + // let select the next avaliable Authorization server + authorization_service_error = true; + + model.notify(); + }); + } + + // task to fetch public keys + pplx::task request_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, pubkeys_shared_state& pubkeys_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + const auto fetch_interval_min(nmos::experimental::fields::fetch_authorization_public_keys_interval_min(model.settings)); + const auto fetch_interval_max(nmos::experimental::fields::fetch_authorization_public_keys_interval_max(model.settings)); + + // start a background task to fetch public keys on a time interval + return pplx::do_while([=, &model, &authorization_state, &pubkeys_state, &gate] + { + auto fetch_interval = std::chrono::seconds(0); + if (nmos::with_read_lock(authorization_state.mutex, [&] + { + auto issuer = authorization_state.issuers.find(pubkeys_state.issuer.to_string()); + return ((authorization_state.issuers.end() != issuer) && !pubkeys_state.one_shot && !pubkeys_state.immediate); + })) + { + fetch_interval = std::chrono::seconds((int)(std::uniform_real_distribution<>(fetch_interval_min, fetch_interval_max)(pubkeys_state.engine))); + } + pubkeys_state.immediate = false; + + slog::log(gate, SLOG_FLF) << "Requesting authorization public keys (jwks) for about " << fetch_interval.count() << " seconds"; + + auto fetch_time = std::chrono::steady_clock::now(); + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() mutable + { + auto lock = authorization_state.read_lock(); + + return details::request_jwks(*pubkeys_state.client, pubkeys_state.version, gate, token); + + }).then([&authorization_state, &pubkeys_state, &gate](web::json::value jwks_) + { + auto lock = authorization_state.write_lock(); + + const auto& jwks = nmos::experimental::get_jwks(authorization_state, pubkeys_state.issuer); + + // are changes found in new set of jwks? + if(jwks != jwks_) + { + // convert jwks to array of public keys + auto pems = web::json::value::array(); + for (const auto& jwk : jwks_.as_array()) + { + try + { + const auto pem = jwk_to_public_key(jwk); // can throw jwk_exception + + web::json::push_back(pems, web::json::value_of({ + { U("jwk"), jwk }, + { U("pem"), pem } + })); + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(pubkeys_state.issuer.to_string()) << " JWK error: " << e.what(); + } + } + + // update jwks and jwt validator cache + if (pems.as_array().size()) + { + const auto auth_version = pubkeys_state.version; + nmos::experimental::update_jwks(authorization_state, pubkeys_state.issuer, jwks_, nmos::experimental::jwt_validator(pems, [auth_version](const web::json::value& payload) + { + // validate access token payload JSON + authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(auth_version)); // may throw json_exception + })); + + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys from " << utility::us2s(pubkeys_state.issuer.to_string()); + } + else + { + nmos::experimental::erase_jwks(authorization_state, pubkeys_state.issuer); + + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list from " << utility::us2s(pubkeys_state.issuer.to_string()); + } + } + else + { + slog::log(gate, SLOG_FLF) << "No public keys changes found from " << utility::us2s(pubkeys_state.issuer.to_string()); + } + + return !pubkeys_state.one_shot; + }); + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + pubkeys_state.received = true; + authorization_service_error = false; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JSON error: " << e.what(); + authorization_service_error = true; + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request OAuth 2.0 error: " << e.what(); + authorization_service_error = true; + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JWK error: " << e.what(); + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error: " << e.what(); + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error"; + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request unexpected unknown exception"; + } + + model.notify(); + }); + } + + // fetch authorization server metadata, such as endpoints use for client registration, token fetch and public keys fetch + bool request_authorization_server_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, bool& authorization_service_error, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization server metadata fetch"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + std::unique_ptr client; + bool metadata_received(false); + + pplx::cancellation_token_source cancellation_source; + pplx::task request = pplx::task_from_result(); + + std::set scopes; + std::set grants; + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method; + with_read_lock(authorization_state.mutex, [&] + { + const auto& client_metadata = nmos::experimental::get_client_metadata(authorization_state); + scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); + token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); + }); + + for (;;) + { + // wait for the thread to be interrupted because an error has been encountered with the selected authorization service + // or because the server is being shut down + condition.wait(lock, [&] { return shutdown || authorization_service_error || metadata_received || !client; }); + if (authorization_service_error) + { + pop_authorization_service(model.settings); + model.notify(); + authorization_service_error = false; + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + + request.wait(); + + client.reset(); + cancellation_source = pplx::cancellation_token_source(); + } + if (shutdown || empty_authorization_services(model.settings) || metadata_received) break; + + const auto service = top_authorization_service(model.settings); + + const auto auth_uri = service.second; + client.reset(new web::http::client::http_client(auth_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate))); + + auto token = cancellation_source.get_token(); + + const auto auth_version = service.first.first; + request = details::request_authorization_server_metadata(*client, scopes, grants, token_endpoint_auth_method, auth_version, gate, token).then([&authorization_state](web::json::value metadata) + { + auto lock = authorization_state.write_lock(); + + // record the current authorization server + authorization_state.authorization_server_uri = nmos::experimental::fields::issuer(metadata); + + // cache the authorization server metadata + nmos::experimental::update_authorization_server_metadata(authorization_state, metadata); + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + metadata_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata unexpected unknown exception"; + + authorization_service_error = true; + } + }); + request.then([&] + { + condition.notify_all(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || metadata_received; }); + } + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + return !authorization_service_error && metadata_received; + } + + // fetch client metadata via OpenID Connect server + bool request_client_metadata_from_openid_connect(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler save_authorization_client, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting OpenID Connect client metadata fetch"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task request = pplx::task_from_result(); + + bool registered(false); + + web::json::value client_metadata{}; + web::json::value authorization_server_metadata{}; + with_read_lock(authorization_state.mutex, [&] + { + client_metadata = nmos::experimental::get_client_metadata(authorization_state); + authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); + }); + + // is client already registered to the Authorization server + if(client_metadata.is_null()) + { + slog::log(gate, SLOG_FLF) << "Missing client_metadata from cache"; + return false; + } + + const auto& auth_version = version(nmos::experimental::fields::issuer(client_metadata)); + + // See https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse + // registration_access_token + // OPTIONAL. Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the + // Client registration. + // registration_client_uri + // OPTIONAL. Location of the Client Configuration Endpoint where the Registration Access Token can be used to perform subsequent operations + // upon the resulting Client registration. + // Implementations MUST either return both a Client Configuration Endpoint and a Registration Access Token or neither of them. + if (!client_metadata.has_string_field(nmos::experimental::fields::registration_access_token)) + { + slog::log(gate, SLOG_FLF) << "No registration_access_token from client_metadata"; + return false; + } + const auto& registration_access_token = nmos::experimental::fields::registration_access_token(client_metadata); + if (!client_metadata.has_string_field(nmos::experimental::fields::registration_client_uri)) + { + slog::log(gate, SLOG_FLF) << "No registration_client_uri from client_metadata"; + return false; + } + const auto& registration_client_uri = nmos::experimental::fields::registration_client_uri(client_metadata); + const auto& issuer = nmos::experimental::fields::issuer(authorization_server_metadata); + + request = request_client_metadata_from_openid_connect(web::http::client::http_client(registration_client_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, make_authorization_config_handler(authorization_server_metadata, client_metadata, gate), { registration_access_token }, gate)), + auth_version, gate, token).then([&model, &authorization_state, issuer, save_authorization_client, &gate](web::json::value client_metadata) + { + auto lock = model.write_lock(); + + if ((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::none.name) + && (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name) + && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) + { + slog::log(gate, SLOG_FLF) << "Missing client_secret"; + throw authorization_exception(); + } + + // scope is optional, it may not be returned by the Authorization server, just insert it, + // as it is required for the authorization support + if (!client_metadata.has_field(nmos::experimental::fields::scope)) + { + client_metadata[nmos::experimental::fields::scope] = web::json::value::string(make_scope(nmos::experimental::authorization_scopes::from_settings(model.settings))); + } + // grant_types is optional, it may not be returned by the Authorization server, just insert it, + // as it is required for the authorization support + if (!client_metadata.has_field(nmos::experimental::fields::grant_types)) + { + client_metadata[nmos::experimental::fields::grant_types] = make_grant_types(grant_types_from_settings(model.settings)); + } + // token_endpoint_auth_method is optional, it may not be returning by the Authorization server, just insert it, + // as it is required for the authorization support + if (!client_metadata.has_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); + } + + // store client_credentials to cache + // hmm, may store the only required fields + with_write_lock(authorization_state.mutex, [&] + { + nmos::experimental::update_client_metadata(authorization_state, client_metadata); + }); + + // do callback to safely store the client metadata + // Client metadata SHOULD be stored by the client in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. + // Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials + if (save_authorization_client) + { + save_authorization_client(web::json::value_of({ + { nmos::experimental::fields::authorization_server_uri, issuer }, + { nmos::experimental::fields::client_metadata, client_metadata } + })); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + registered = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve unexpected unknown exception"; + + authorization_service_error = true; + } + + model.notify(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || registered; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + return !authorization_service_error && registered; + } + + // register client to the Authorization server + bool client_registration(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler save_authorization_client, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization client registration"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task request = pplx::task_from_result(); + + bool registered(false); + + const auto auth_version = top_authorization_service(model.settings).first.first; + + // create client metadata from settings + // see https://tools.ietf.org/html/rfc7591#section-2 + const auto client_name = model.settings.has_field(nmos::fields::label) ? nmos::fields::label(model.settings) : U(""); + const std::vector redirect_uris = { make_authorization_redirect_uri(model.settings) }; + const auto scopes = nmos::experimental::authorization_scopes::from_settings(model.settings); + const auto grants = grant_types_from_settings(model.settings); + + std::set response_types; + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return web::http::oauth2::experimental::grant_types::authorization_code == grant; })) + { + response_types.insert(web::http::oauth2::experimental::response_types::code); + } + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return web::http::oauth2::experimental::grant_types::implicit == grant; })) + { + response_types.insert(web::http::oauth2::experimental::response_types::token); + } + if (response_types.empty()) + { + response_types.insert(web::http::oauth2::experimental::response_types::none); + } + + const auto token_endpoint_auth_method = token_endpoint_auth_method_from_settings(model.settings); + const auto& authorization_server_metadata = with_read_lock(authorization_state.mutex, [&] { return get_authorization_server_metadata(authorization_state); }); + const auto& registration_endpoint = web::uri(nmos::experimental::fields::registration_endpoint(authorization_server_metadata)); + const auto& issuer = nmos::experimental::fields::issuer(authorization_server_metadata); + const auto jwks_uri = make_jwks_uri(model.settings); + const auto& initial_access_token = nmos::experimental::fields::initial_access_token(model.settings); + + request = request_client_registration(web::http::client::http_client(registration_endpoint, make_authorization_http_client_config(model.settings, load_ca_certificates, make_authorization_config_handler({}, {}, gate), { initial_access_token }, gate)), + client_name, redirect_uris, {}, response_types, scopes, grants, token_endpoint_auth_method, {}, jwks_uri, auth_version, gate, token).then([&model, &authorization_state, issuer, token_endpoint_auth_method, save_authorization_client, &gate](web::json::value client_metadata) + { + auto lock = model.write_lock(); + + // check client_secret existence for confidential client + if (client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + if ((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::none.name) + && (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name) + && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) + { + slog::log(gate, SLOG_FLF) << "Missing client_secret"; + throw authorization_exception(); + } + } + else + { + if ((web::http::oauth2::experimental::token_endpoint_auth_methods::none != token_endpoint_auth_method) + && (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt != token_endpoint_auth_method) + && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) + { + slog::log(gate, SLOG_FLF) << "Missing client_secret"; + throw authorization_exception(); + } + } + + // scope is optional, it may not be returned by the Authorization server, just insert it, + // as it is required for the authorization support + if (!client_metadata.has_field(nmos::experimental::fields::scope)) + { + client_metadata[nmos::experimental::fields::scope] = web::json::value::string(make_scope(nmos::experimental::authorization_scopes::from_settings(model.settings))); + } + // grant_types is optional, it may not be returned by the Authorization server, just insert it, + // as it is required for the authorization support + if (!client_metadata.has_field(nmos::experimental::fields::grant_types)) + { + client_metadata[nmos::experimental::fields::grant_types] = make_grant_types(grant_types_from_settings(model.settings)); + } + // token_endpoint_auth_method is optional, it may not be returning by the Authorization server, just insert it, + // as it is required for the authorization support + if (!client_metadata.has_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); + } + + // store client metadata to settings + // hmm, may store the only required fields + with_write_lock(authorization_state.mutex, [&] + { + nmos::experimental::update_client_metadata(authorization_state, client_metadata); + }); + + // hmm, do a callback allowing user to store the client credentials + // Client credentials SHOULD be stored by the client in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials + if (save_authorization_client) + { + save_authorization_client(web::json::value_of({ + { nmos::experimental::fields::authorization_server_uri, issuer }, + { nmos::experimental::fields::client_metadata, client_metadata } + })); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + registered = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration unexpected unknown exception"; + + authorization_service_error = true; + } + + model.notify(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || registered; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + return !authorization_service_error && registered; + } + + // start authorization code workflow + // see https://tools.ietf.org/html/rfc8252#section-4.1 + bool authorization_code_flow(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::request_authorization_code_handler request_authorization_code, slog::base_gate& gate) + { + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + const auto& settings = model.settings; + + const auto authorization_server_metadata = with_read_lock(authorization_state.mutex, [&] { return get_authorization_server_metadata(authorization_state); }); + const web::uri authorization_endpoint(nmos::experimental::fields::authorization_endpoint(authorization_server_metadata)); + const auto code_challenge_methods_supported(nmos::experimental::fields::code_challenge_methods_supported(authorization_server_metadata)); + + const auto client_metadata = with_read_lock(authorization_state.mutex, [&] { return get_client_metadata(authorization_state); }); + const auto client_id(nmos::experimental::fields::client_id(client_metadata)); + const web::uri redirct_uri(nmos::experimental::fields::redirect_uris(client_metadata).size() ? nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string() : U("")); + const auto scopes = nmos::experimental::details::scopes(nmos::experimental::fields::scope(client_metadata)); + + slog::log(gate, SLOG_FLF) << "Attempting authorization code flow for scope: '" << nmos::experimental::details::make_scope(scopes) << "'"; + + // start the authorization code grant workflow, the authorization URI is required to + // be loaded in the web browser to kick start the authorization code grant workflow + if (request_authorization_code) + { + nmos::with_write_lock(authorization_state.mutex, [&] + { + authorization_state.authorization_flow = nmos::experimental::authorization_state::request_code; + request_authorization_code(make_authorization_code_uri(authorization_endpoint, client_id, redirct_uri, web::http::oauth2::experimental::response_types::code, scopes, code_challenge_methods_supported, authorization_state.state, authorization_state.code_verifier)); + }); + + // wait until received access token, or timeout + const auto& authorization_code_flow_max = nmos::experimental::fields::authorization_code_flow_max(settings); + if (authorization_code_flow_max > -1) + { + // wait access token with timeout + if (!model.wait_for(lock, std::chrono::seconds(authorization_code_flow_max), [&] { return shutdown || nmos::experimental::authorization_state::failed == authorization_state.authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_state.authorization_flow; })) + { + // authorization code workflow timeout + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "authorization code workflow timeout"; + } + } + else + { + // wait access token without timeout + condition.wait(lock, [&] { return shutdown || nmos::experimental::authorization_state::failed == authorization_state.authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_state.authorization_flow; }); + } + } + else + { + // no handler to start the authorization code grant workflow + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "no authorization code workflow handler"; + } + + model.notify(); + + return !authorization_service_error && nmos::experimental::authorization_state::access_token_received == authorization_state.authorization_flow; + } + + // fetch the bearer access token for the required scope(s) to access the protected APIs + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#requesting-a-token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#accessing-protected-resources + // fetch the token issuer(authorization server)'s public keys fpr validating the incoming bearer access token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization operation: " << (immediate_token_fetch ? "immediate fetch token" : "fetch token at next interval"); + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + const auto authorization_server_metadata = with_read_lock(authorization_state.mutex, [&] { return get_authorization_server_metadata(authorization_state); }); + const web::uri jwks_uri(nmos::experimental::fields::jwks_uri(authorization_server_metadata)); + const web::uri token_endpoint(nmos::experimental::fields::token_endpoint(authorization_server_metadata)); + const auto& authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); + const auto& grant = (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? web::http::oauth2::experimental::grant_types::client_credentials : web::http::oauth2::experimental::grant_types::authorization_code; + const auto authorization_version = top_authorization_service(model.settings).first.first; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + + auto pubkeys_request(pplx::task_from_result()); + + with_write_lock(authorization_state.mutex, [&] + { + auto& pubkeys_state = authorization_state.pubkeys_state; + pubkeys_state.client.reset(new web::http::client::http_client{ jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }); + pubkeys_state.version = authorization_version; + pubkeys_state.issuer = nmos::experimental::fields::issuer(authorization_server_metadata); + }); + + auto bearer_token_request(pplx::task_from_result()); + web::http::oauth2::experimental::oauth2_token bearer_token; + std::set scopes; + nmos::with_read_lock(authorization_state.mutex, [&] + { + bearer_token = authorization_state.bearer_token.is_valid_access_token() ? authorization_state.bearer_token : web::http::oauth2::experimental::oauth2_token{}; + const auto& client_metadata = nmos::experimental::get_client_metadata(authorization_state); + scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + }); + token_shared_state token_state( + grant, + bearer_token, + { token_endpoint, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, + authorization_version, + std::move(load_rsa_private_keys), + immediate_token_fetch + ); + + auto token = cancellation_source.get_token(); + + // start a background task to fetch public keys from authorization server + if (nmos::experimental::fields::server_authorization(model.settings)) + { + pubkeys_request = request_public_keys(model, authorization_state, authorization_state.pubkeys_state, authorization_service_error, gate, token); + } + + // start a background task to fetch bearer access token from authorization server + if (nmos::experimental::fields::client_authorization(model.settings) && scopes.size()) + { + bearer_token_request = request_token(model, authorization_state, token_state, authorization_service_error, gate, token); + } + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + pubkeys_request.wait(); + bearer_token_request.wait(); + } + + // make an asynchronously GET request over the Token Issuer to fetch issuer metadata + bool request_token_issuer_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + std::unique_ptr client; + bool metadata_received(false); + + pplx::cancellation_token_source cancellation_source; + + // wait for the thread to be interrupted because of no matching public keys from the received token or because the server is being shut down + condition.wait(lock, [&] { return shutdown || nmos::with_read_lock(authorization_state.mutex, [&] { return authorization_state.fetch_token_issuer_pubkeys; }); }); + + if (shutdown) return false; + + slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer metadata fetch"; + + web::uri token_issuer; + std::set scopes; + std::set grants; + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method; + nmos::with_write_lock(authorization_state.mutex, [&] + { + authorization_state.fetch_token_issuer_pubkeys = false; + token_issuer = authorization_state.token_issuer; + + const auto& client_metadata = nmos::experimental::get_client_metadata(authorization_state); + scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); + token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); + }); + + if (token_issuer.is_empty()) return false; + + slog::log(gate, SLOG_FLF) << "Using authorization token's issuer " << utility::us2s(token_issuer.to_string()) << " to fetch server metadata"; + client.reset(new web::http::client::http_client(make_authorization_service_uri(token_issuer), make_authorization_http_client_config(model.settings, load_ca_certificates, gate))); + + auto token = cancellation_source.get_token(); + + auto request = details::request_authorization_server_metadata(*client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&](web::json::value metadata) + { + auto lock = authorization_state.write_lock(); + + // cache the issuer metadata + nmos::experimental::update_authorization_server_metadata(authorization_state, token_issuer, metadata); + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + metadata_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request unexpected unknown exception"; + + authorization_service_error = true; + } + }); + request.then([&] + { + condition.notify_all(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || metadata_received; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + return !authorization_service_error && metadata_received; + } + + // make an asynchronously GET request over the Token Issuer to fetch public keys + void request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer's public keys"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + + web::uri token_issuer; + web::uri jwks_uri; + with_read_lock(authorization_state.mutex, [&] + { + token_issuer = authorization_state.token_issuer; + jwks_uri = nmos::experimental::fields::jwks_uri(get_authorization_server_metadata(authorization_state, token_issuer)); + }); + auto authorization_version = version(token_issuer); + pubkeys_shared_state pubkeys_state( + { jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, + authorization_version, + token_issuer, + true + ); + + // update the authorization_behaviour_thread's fetch public keys shared state, public keys are going to be fetched from this token issuer from now on + with_write_lock(authorization_state.mutex, [&] + { + auto& pubkeys_state = authorization_state.pubkeys_state; + pubkeys_state.client.reset(new web::http::client::http_client{ jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }); + pubkeys_state.version = authorization_version; + pubkeys_state.issuer = token_issuer; + }); + + auto token = cancellation_source.get_token(); + + // start a one-shot background task to fetch public keys from the token issuer + auto pubkeys_request = request_public_keys(model, authorization_state, pubkeys_state, authorization_service_error, gate, token); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || pubkeys_state.received; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + pubkeys_request.wait(); + } + } + } +} diff --git a/Development/nmos/authorization_operation.h b/Development/nmos/authorization_operation.h new file mode 100644 index 000000000..c3d8b10eb --- /dev/null +++ b/Development/nmos/authorization_operation.h @@ -0,0 +1,70 @@ +#ifndef NMOS_AUTHORIZATION_OPERATION_H +#define NMOS_AUTHORIZATION_OPERATION_H + +#include "nmos/authorization_behaviour.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + namespace details + { + // construct authorization client config based on settings + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); + inline web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_authorization_http_client_config(settings, load_ca_certificates, {}, {}, gate); + } + + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + // this function is based on the oauth2::token_from_redirected_uri + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication + // this function is based on the oauth2::token_from_redirected_uri + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const utility::string_t& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + // make an asynchronously GET request on the Authorization API to fetch authorization server metadata + bool request_authorization_server_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, bool& authorization_service_error, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + // make an asynchronously GET request on the OpenID Authorization API to fetch client metadata + bool request_client_metadata_from_openid_connect(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler client_registered, slog::base_gate& gate); + + // make an asynchronously POST request on the Authorization API to register a client + // see https://tools.ietf.org/html/rfc6749#section-2 + // see https://tools.ietf.org/html/rfc7591#section-3.1 + bool client_registration(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler client_registered, slog::base_gate& gate); + + // start authorization code workflow + // see https://tools.ietf.org/html/rfc8252#section-4.1 + bool authorization_code_flow(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::request_authorization_code_handler request_authorization_code, slog::base_gate& gate); + + // The bearer token is used for accessing protected APIs + // The pems are used for validating incoming access token + // fetch the bearer access token for the required scope(s) to access the protected APIs + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#requesting-a-token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#accessing-protected-resources + // fetch the Token Issuer(authorization server)'s public keys fpr validating the incoming bearer access token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate); + + // make an asynchronously GET request over the Token Issuer to fetch issuer metadata + bool request_token_issuer_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + // make an asynchronously GET request over the Token Issuer to fetch public keys + void request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + } + } +} + +#endif diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp new file mode 100644 index 000000000..15334a033 --- /dev/null +++ b/Development/nmos/authorization_redirect_api.cpp @@ -0,0 +1,193 @@ +#include "nmos/authorization_redirect_api.h" + +#include "cpprest/response_type.h" +#include "nmos/api_utils.h" +#include "nmos/authorization_behaviour.h" // for top_authorization_service +#include "nmos/authorization_operation.h" // for request_token_from_redirected_uri +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/certificate_settings.h" +#include "nmos/client_utils.h" // for make_http_client_config +#include "nmos/jwt_generator.h" +#include "nmos/jwk_utils.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + typedef std::pair authorization_flow_response; + + inline authorization_flow_response make_authorization_flow_error_response(web::http::status_code code, const utility::string_t& error = {}, const utility::string_t& debug = {}) + { + return{ code, make_error_response_body(code, error, debug) }; + } + + inline authorization_flow_response make_authorization_flow_error_response(web::http::status_code code, const std::exception& debug) + { + return make_authorization_flow_error_response(code, {}, utility::s2us(debug.what())); + } + } + + web::http::experimental::listener::api_router make_authorization_redirect_api(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate_) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router authorization_api; + + authorization_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-authorization/") }, req, res)); + return pplx::task_from_result(true); + }); + + authorization_api.support(U("/x-authorization/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("callback/"), U("jwks/")}, req, res)); + return pplx::task_from_result(true); + }); + + authorization_api.support(U("/x-authorization/callback/?"), methods::GET, [&model, &authorization_state, load_ca_certificates, load_rsa_private_keys, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + details::authorization_flow_response result{ status_codes::BadRequest, {} }; + + utility::string_t state; + utility::string_t code_verifier; + web::json::value authorization_server_metadata; + web::json::value client_metadata; + with_write_lock(authorization_state.mutex, [&] + { + state = authorization_state.state; + code_verifier = authorization_state.code_verifier; + authorization_state.authorization_flow = authorization_state::request_code; + authorization_server_metadata = get_authorization_server_metadata(authorization_state); + client_metadata = get_client_metadata(authorization_state); + }); + + web::uri token_endpoint; + web::http::client::http_client_config config; + utility::string_t response_type; + nmos::api_version version; + utility::string_t client_id; + utility::string_t client_secret; + utility::string_t scope; + utility::string_t redirect_uri; + utility::string_t token_endpoint_auth_method; + utility::string_t rsa_private_key; + auto client_assertion_lifespan(std::chrono::seconds(30)); + with_read_lock(model.mutex, [&, load_ca_certificates, load_rsa_private_keys] + { + const auto& settings = model.settings; + token_endpoint = nmos::experimental::fields::token_endpoint(authorization_server_metadata); + config = nmos::make_http_client_config(settings, load_ca_certificates, gate); + response_type = web::http::oauth2::experimental::response_types::code.name; + client_id = nmos::experimental::fields::client_id(client_metadata); + client_secret = client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""); + scope = nmos::experimental::fields::scope(client_metadata); + redirect_uri = nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string(); + version = details::top_authorization_service(model.settings).first.first; + token_endpoint_auth_method = client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method) ? nmos::experimental::fields::token_endpoint_auth_method(client_metadata) : web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name; + + if (load_rsa_private_keys) + { + // get RSA private key from list + rsa_private_key = details::found_rsa_key(load_rsa_private_keys()); + } + client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings)); + }); + + web::http::client::http_client client(token_endpoint, config); + + auto request_token = pplx::task_from_result(web::http::oauth2::experimental::oauth2_token()); + + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + { + const auto client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key); + + // exchange authorization code for bearer token + // where redirected URI: /x-authorization/callback/?state=&code= + request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, scope, redirect_uri, state, code_verifier, token_endpoint_auth_method, client_assertion, gate); + } + else + { + // exchange authorization code for bearer token + // where redirected URI: /x-authorization/callback/?state=&code= + request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, client_secret, scope, redirect_uri, state, code_verifier, gate); + } + + auto request = request_token.then([&model, &authorization_state, &scope, &gate](web::http::oauth2::experimental::oauth2_token bearer_token) + { + auto lock = authorization_state.write_lock(); + + // signal authorization_flow that bearer token has just been received + authorization_state.authorization_flow = authorization_state::access_token_received; + + // update bearer token cache, which will be used for accessing protected APIs + authorization_state.bearer_token = bearer_token; + + slog::log(gate, SLOG_FLF) << utility::us2s(bearer_token.scope()) << " bearer token updated"; + + }).then([&](pplx::task finally) + { + auto lock = authorization_state.write_lock(); // in order to update shared state + + try + { + finally.get(); + result = { status_codes::OK, web::json::value_of({U("Bearer token received")}) }; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + result = details::make_authorization_flow_error_response(status_codes::BadRequest, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request JSON error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request OAuth 2.0 error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request error"; + result = details::make_authorization_flow_error_response(status_codes::InternalError); + authorization_state.authorization_flow = authorization_state::failed; + } + + with_write_lock(model.mutex, [&] + { + model.notify(); + }); + }); + + // hmm, perhaps wait with timeout? + request.wait(); + + if (web::http::is_success_status_code(result.first)) + { + set_reply(res, result.first, result.second); + } + else + { + set_reply(res, result.first, !result.second.is_null() ? result.second : nmos::make_error_response_body(result.first)); + } + + return pplx::task_from_result(true); + }); + + return authorization_api; + } + } +} diff --git a/Development/nmos/authorization_redirect_api.h b/Development/nmos/authorization_redirect_api.h new file mode 100644 index 000000000..780a77650 --- /dev/null +++ b/Development/nmos/authorization_redirect_api.h @@ -0,0 +1,28 @@ +#ifndef NMOS_AUTHORIZATION_REDIRECT_API_H +#define NMOS_AUTHORIZATION_REDIRECT_API_H + +#include "cpprest/api_router.h" +#include "nmos/certificate_handlers.h" + +namespace slog +{ + class base_gate; +} + +// This is an experimental extension to support authorization code via a REST API +namespace nmos +{ + namespace experimental + { + struct authorization_state; + } + + struct base_model; + + namespace experimental + { + web::http::experimental::listener::api_router make_authorization_redirect_api(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/authorization_scopes.h b/Development/nmos/authorization_scopes.h new file mode 100644 index 000000000..98bf172e7 --- /dev/null +++ b/Development/nmos/authorization_scopes.h @@ -0,0 +1,26 @@ +#ifndef NMOS_AUTHORIZATION_SCOPES_H +#define NMOS_AUTHORIZATION_SCOPES_H + +#include +#include +#include "nmos/scope.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace experimental + { + namespace authorization_scopes + { + // get scope set from settings + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::experimental::fields::authorization_scopes) + ? boost::copy_range>(nmos::experimental::fields::authorization_scopes(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::experimental::parse_scope(v.as_string()); })) + : std::set{}; + } + } + } +} + +#endif diff --git a/Development/nmos/authorization_state.cpp b/Development/nmos/authorization_state.cpp new file mode 100644 index 000000000..f4c03b135 --- /dev/null +++ b/Development/nmos/authorization_state.cpp @@ -0,0 +1,164 @@ +#include "nmos/authorization_state.h" + +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + return nmos::experimental::fields::authorization_server_metadata(issuer->second.settings); + } + return{}; + } + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state) + { + return get_authorization_server_metadata(authorization_state, authorization_state.authorization_server_uri); + } + + web::json::value get_client_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + return nmos::experimental::fields::client_metadata(issuer->second.settings); + } + return{}; + } + web::json::value get_client_metadata(const authorization_state& authorization_state) + { + return get_client_metadata(authorization_state, authorization_state.authorization_server_uri); + } + + web::json::value get_jwks(const authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + return nmos::experimental::fields::jwks(issuer->second.settings); + } + return{}; + } + web::json::value get_jwks(const authorization_state& authorization_state) + { + return get_jwks(authorization_state, authorization_state.authorization_server_uri); + } + + void update_authorization_server_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& authorization_server_metadata) + { + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // update + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::authorization_server_metadata] = authorization_server_metadata; + } + else + { + // insert + authorization_state.issuers.insert(std::make_pair( + authorization_server_uri.to_string(), + { web::json::value_of({ + { nmos::experimental::fields::authorization_server_metadata, authorization_server_metadata }, + { nmos::experimental::fields::jwks, {} }, + { nmos::experimental::fields::client_metadata, {} } + }), nmos::experimental::jwt_validator{} } + )); + } + } + void update_authorization_server_metadata(authorization_state& authorization_state, const web::json::value& authorization_server_metadata) + { + update_authorization_server_metadata(authorization_state, authorization_state.authorization_server_uri, authorization_server_metadata); + } + + void update_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& client_metadata) + { + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // update + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::client_metadata] = client_metadata; + } + else + { + // insert + authorization_state.issuers.insert(std::make_pair( + authorization_server_uri.to_string(), + { web::json::value_of({ + { nmos::experimental::fields::authorization_server_metadata, {} }, + { nmos::experimental::fields::jwks, {} }, + { nmos::experimental::fields::client_metadata, client_metadata } + }), nmos::experimental::jwt_validator{} } + )); + } + } + void update_client_metadata(authorization_state& authorization_state, const web::json::value& client_metadata) + { + update_client_metadata(authorization_state, authorization_state.authorization_server_uri, client_metadata); + } + + void update_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator) + { + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // update + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::jwks] = jwks; + issuer->second.jwt_validator = jwt_validator; + } + else + { + // insert + authorization_state.issuers.insert(std::make_pair( + authorization_server_uri.to_string(), + { web::json::value_of({ + { nmos::experimental::fields::authorization_server_metadata,{} }, + { nmos::experimental::fields::jwks, jwks }, + { nmos::experimental::fields::client_metadata,{} } + }), jwt_validator })); + } + } + void update_jwks(authorization_state& authorization_state, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator) + { + update_jwks(authorization_state, authorization_state.authorization_server_uri, jwks, jwt_validator); + } + + void erase_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // erase + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::client_metadata] = {}; + } + } + void erase_client_metadata(authorization_state& authorization_state) + { + erase_client_metadata(authorization_state, authorization_state.authorization_server_uri); + } + + void erase_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // erase + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::jwks] = {}; + issuer->second.jwt_validator = {}; + } + } + void erase_jwks(authorization_state& authorization_state) + { + erase_jwks(authorization_state, authorization_state.authorization_server_uri); + } + } +} + diff --git a/Development/nmos/authorization_state.h b/Development/nmos/authorization_state.h new file mode 100644 index 000000000..06d326344 --- /dev/null +++ b/Development/nmos/authorization_state.h @@ -0,0 +1,118 @@ +#ifndef NMOS_AUTHORIZATION_STATE_H +#define NMOS_AUTHORIZATION_STATE_H + +#include +#include "cpprest/http_client.h" +#include "cpprest/oauth2.h" +#include "nmos/api_version.h" +#include "nmos/issuers.h" +#include "nmos/mutex.h" +#include "nmos/random.h" + +namespace nmos +{ + namespace experimental + { + struct pubkeys_shared_state + { + nmos::details::seed_generator seeder; + std::default_random_engine engine; + std::unique_ptr client; + nmos::api_version version; + web::uri issuer; + bool one_shot; // how offen to fetch the public keys, where true = one-shot; false = loop based on time interval + bool immediate; // true = do an immediate fetch; false = do time interval fetch + bool received; + + pubkeys_shared_state() + : engine(seeder) + , one_shot(false) + , immediate(true) + , received(false) {} + + pubkeys_shared_state(web::http::client::http_client client, nmos::api_version version, web::uri issuer, bool one_shot = false) + : engine(seeder) + , client(std::unique_ptr(new web::http::client::http_client(client))) + , version(std::move(version)) + , issuer(std::move(issuer)) + , one_shot(one_shot) + , immediate(true) + , received(false) {} + }; + + struct authorization_state + { + // mutex to be used to protect the members of the settings from simultaneous access by multiple threads + mutable nmos::mutex mutex; + + // authorization code flow settings + utility::string_t state; + utility::string_t code_verifier; + + enum authorization_flow_type + { + request_code, + exchange_code_for_access_token, + fetch_access_token, + access_token_received, + failed + }; + // current status of the authorization flow + authorization_flow_type authorization_flow; + + // fetch public keys from token issuer(Authorization server), in event when no matching keys in cache to validate token + // it is used for triggering the authorization_token_issuer_thread to fetch the token issuer metadata follow by fetching the issuer public keys + bool fetch_token_issuer_pubkeys; + web::uri token_issuer; + + // map of issuer (authorization server) to jwt_validator set for access token validation + nmos::experimental::issuers issuers; + // the authorization server which is currently connected to + web::uri authorization_server_uri; + + // OAuth 2.0 bearer token to access authorizaton protected APIs + web::http::oauth2::experimental::oauth2_token bearer_token; + + // shared state for the public keys fetch + pubkeys_shared_state pubkeys_state; + + nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } + nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } + + authorization_state() + : state{} + , code_verifier{} + , authorization_flow(request_code) + , fetch_token_issuer_pubkeys{ false } + , token_issuer{} + , authorization_server_uri{} + {} + }; + + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri); + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state); + + web::json::value get_client_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri); + web::json::value get_client_metadata(const authorization_state& authorization_state); + + web::json::value get_jwks(const authorization_state& authorization_state, const web::uri& authorization_server_uri); + web::json::value get_jwks(const authorization_state& authorization_state); + + void update_authorization_server_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& authorization_server_metadata); + void update_authorization_server_metadata(authorization_state& authorization_state, const web::json::value& authorization_server_metadata); + + void update_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& client_metadata); + void update_client_metadata(authorization_state& authorization_state, const web::json::value& client_metadata); + + void update_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator); + void update_jwks(authorization_state& authorization_state, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator); + + void erase_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri); + void erase_client_metadata(authorization_state& authorization_state); + + void erase_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri); + void erase_jwks(authorization_state& authorization_state); + } +} + +#endif diff --git a/Development/nmos/authorization_utils.cpp b/Development/nmos/authorization_utils.cpp new file mode 100644 index 000000000..add0d15e0 --- /dev/null +++ b/Development/nmos/authorization_utils.cpp @@ -0,0 +1,90 @@ +#include "nmos/authorization_utils.h" + +#include // for boost::is_any_of +#include // for boost::split +#include "cpprest/client_type.h" +#include "cpprest/base_uri.h" +#include "nmos/authorization_scopes.h" +#include "nmos/is10_versions.h" +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + // get grant type set from json array + std::set grant_types(const web::json::array& grants) + { + return boost::copy_range>(grants | boost::adaptors::transformed([](const web::json::value& v) { return web::http::oauth2::experimental::parse_grant(v.as_string()); })); + } + + // get grant type set from settings + std::set grant_types_from_settings(const nmos::settings& settings) + { + const auto& authorization_flow = nmos::experimental::fields::authorization_flow(settings); + return (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? std::set{ web::http::oauth2::experimental::grant_types::client_credentials } : std::set{ web::http::oauth2::experimental::grant_types::authorization_code, web::http::oauth2::experimental::grant_types::refresh_token }; + } + + // get grant type set from client metadata if presented, otherwise return default grant types + std::set grant_types(const web::json::value& client_metadata, const std::set& default_grant_types) + { + if (!client_metadata.is_null() && client_metadata.has_array_field(nmos::experimental::fields::grant_types)) + { + return details::grant_types(nmos::experimental::fields::grant_types(client_metadata)); + } + return default_grant_types; + } + + // get scope set from a spare delimiter scope string + std::set scopes(const utility::string_t& scope) + { + std::vector tokens; + boost::split(tokens, scope, boost::is_any_of(U(" "))); + return boost::copy_range>(tokens | boost::adaptors::transformed([](const utility::string_t& v) { return parse_scope(v); })); + } + + // get scope set from client metadata if presented, otherwise return default scope set + std::set scopes(const web::json::value& client_metadata, const std::set& default_scopes) + { + if (!client_metadata.is_null() && client_metadata.has_string_field(nmos::experimental::fields::scope)) + { + return scopes(nmos::experimental::fields::scope(client_metadata)); + } + return default_scopes; + } + + // get token_endpoint_auth_method from settings + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method_from_settings(const nmos::settings& settings) + { + return web::http::oauth2::experimental::parse_token_endpoint_auth_method(nmos::experimental::fields::token_endpoint_auth_method(settings)); + } + + // get token_endpoint_auth_method from client metadata if presented, otherwise return default_token_endpoint_auth_method + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method(const web::json::value& client_metadata, const web::http::oauth2::experimental::token_endpoint_auth_method& default_token_endpoint_auth_method) + { + namespace token_endpoint_auth_methods = web::http::oauth2::experimental::token_endpoint_auth_methods; + + auto token_endpoint_auth_method = default_token_endpoint_auth_method; + if (!client_metadata.is_null() && client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + token_endpoint_auth_method = web::http::oauth2::experimental::token_endpoint_auth_method{ nmos::experimental::fields::token_endpoint_auth_method(client_metadata) }; + } + return token_endpoint_auth_method; + } + + // get issuer version + api_version version(const web::uri& issuer) + { + // issuer uri should be like "https://server.example.com/{version} + api_version ver{ api_version{} }; + if (!issuer.is_path_empty()) + { + ver = parse_api_version(web::uri::split_path(issuer.path()).back()); + } + return (api_version{} == ver) ? is10_versions::v1_0 : ver; + } + } + } +} diff --git a/Development/nmos/authorization_utils.h b/Development/nmos/authorization_utils.h new file mode 100644 index 000000000..dfce4cd74 --- /dev/null +++ b/Development/nmos/authorization_utils.h @@ -0,0 +1,61 @@ +#ifndef NMOS_AUTHORIZATION_UTILS_H +#define NMOS_AUTHORIZATION_UTILS_H + +#include +#include "cpprest/grant_type.h" +#include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_version.h" +#include "nmos/scope.h" +#include "nmos/settings.h" // just a forward declaration of nmos::settings + +namespace web +{ + class uri; +} + +namespace nmos +{ + namespace experimental + { + namespace details + { + // get client's grant types + std::set grant_types(const web::json::array& grants); + std::set grant_types_from_settings(const nmos::settings& settings); + std::set grant_types(const web::json::value& client_metadata, const std::set& default_grant_types); + // get client's scopes + std::set scopes(const utility::string_t& scope); + std::set scopes(const web::json::value& client_metadata, const std::set& default_scopes); + // get client's token_endpoint_auth_method + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method_from_settings(const nmos::settings& settings); + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method(const web::json::value& client_metadata, const web::http::oauth2::experimental::token_endpoint_auth_method& default_token_endpoint_auth_method); + // get issuer version + api_version version(const web::uri& issuer); + + // is subsets found in given set + template + inline bool find_all(const std::set& sub, const std::set& full) + { + if (sub.size() == 0 || full.size() == 0) { return false; } + + for (auto s : sub) + { + bool found{ false }; + for (auto f : full) + { + if (f == s) + { + found = true; + break; + } + } + + if (!found) { return false; } + } + return true; + } + } + } +} + +#endif diff --git a/Development/nmos/certificate_handlers.cpp b/Development/nmos/certificate_handlers.cpp index 81064b37e..d91d70a27 100644 --- a/Development/nmos/certificate_handlers.cpp +++ b/Development/nmos/certificate_handlers.cpp @@ -121,4 +121,61 @@ namespace nmos return utility::string_t{}; }; } + + // construct callback to load RSA private keys from file based on settings, see nmos/certificate_settings.h + // require for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint + load_rsa_private_keys_handler make_load_rsa_private_keys_handler(const nmos::settings& settings, slog::base_gate& gate) + { + // load the server private keys from files + auto server_certificates = nmos::experimental::fields::server_certificates(settings); + if (0 == server_certificates.size()) + { + // (deprecated, replaced by server_certificates) + const auto private_key_files = nmos::experimental::fields::private_key_files(settings); + + for (const auto& private_key_file : private_key_files.as_array()) + { + web::json::push_back(server_certificates, + web::json::value_of({ + { nmos::experimental::fields::private_key_file, private_key_file }, + { nmos::experimental::fields::certificate_chain_file, {} } + }) + ); + } + } + + return [&, server_certificates]() + { + slog::log(gate, SLOG_FLF) << "Load server private keys"; + + auto data = std::vector(); + auto private_keys = std::vector(); + if (0 == server_certificates.size()) + { + slog::log(gate, SLOG_FLF) << "Missing server certificates"; + } + + for (const auto& server_certificate : server_certificates.as_array()) + { + const auto key_algorithm = nmos::experimental::fields::key_algorithm(server_certificate); + const auto private_key_file = nmos::experimental::fields::private_key_file(server_certificate); + + utility::stringstream_t pkey; + if (private_key_file.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing private key file"; + } + else + { + if (key_algorithm.empty() || key_algorithms::RSA.name == key_algorithm) + { + utility::ifstream_t pkey_file(private_key_file); + pkey << pkey_file.rdbuf(); + private_keys.push_back(pkey.str()); + } + } + } + return private_keys; + }; + } } diff --git a/Development/nmos/certificate_handlers.h b/Development/nmos/certificate_handlers.h index bb853fef5..043a50e40 100644 --- a/Development/nmos/certificate_handlers.h +++ b/Development/nmos/certificate_handlers.h @@ -65,6 +65,9 @@ namespace nmos // this callback should not throw exceptions typedef std::function load_dh_param_handler; + // callback to supply a list of RSA private keys + typedef std::function()> load_rsa_private_keys_handler; + // construct callback to load certification authorities from file based on settings, see nmos/certificate_settings.h load_ca_certificates_handler make_load_ca_certificates_handler(const nmos::settings& settings, slog::base_gate& gate); @@ -73,6 +76,9 @@ namespace nmos // construct callback to load Diffie-Hellman parameters for ephemeral key exchange support from file based on settings, see nmos/certificate_settings.h load_dh_param_handler make_load_dh_param_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to load server RSA private key files based on settings, see nmos/certificate_settings.h + load_rsa_private_keys_handler make_load_rsa_private_keys_handler(const nmos::settings& settings, slog::base_gate& gate); } #endif diff --git a/Development/nmos/channelmapping_api.cpp b/Development/nmos/channelmapping_api.cpp index dc45d5ece..0439d8a12 100644 --- a/Development/nmos/channelmapping_api.cpp +++ b/Development/nmos/channelmapping_api.cpp @@ -16,7 +16,7 @@ namespace nmos { web::http::experimental::listener::api_router make_unmounted_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate); - web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate) + web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -34,6 +34,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + channelmapping_api.support(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/?"), validate_authorization); + channelmapping_api.support(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is08_versions::from_settings(model.settings); }); channelmapping_api.support(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { diff --git a/Development/nmos/channelmapping_api.h b/Development/nmos/channelmapping_api.h index c3c79d2fb..04cad01df 100644 --- a/Development/nmos/channelmapping_api.h +++ b/Development/nmos/channelmapping_api.h @@ -29,7 +29,12 @@ namespace nmos // Channel Mapping API factory functions // callbacks from this function are called with the model locked, and may read but should not write directly to the model - web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate); + web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate) + { + return make_channelmapping_api(model, std::move(validate_merged), {}, gate); + } inline web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, slog::base_gate& gate) { diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index b2b902379..601a5c3ec 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -7,8 +7,10 @@ #include "cpprest/basic_utils.h" #include "cpprest/details/system_error.h" #include "cpprest/http_utils.h" +#include "cpprest/response_type.h" #include "cpprest/ws_client.h" #include "nmos/certificate_settings.h" +#include "nmos/json_fields.h" #include "nmos/slog.h" #include "nmos/ssl_context_options.h" @@ -58,6 +60,38 @@ namespace nmos #endif } + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) + { + auto config = make_http_client_config(settings, load_ca_certificates, gate); + + if (make_authorization_config && bearer_token.is_valid_access_token()) + { + config.set_oauth2(make_authorization_config(bearer_token)); + } + + return config; + } + + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) + { + auto config = make_http_client_config(settings, load_ca_certificates, gate); + + if (make_authorization_config) + { + auto oauth2_config = make_authorization_config({}); + if (oauth2_config.token().is_valid_access_token()) + { + config.set_oauth2(make_authorization_config({})); + } + } + + return config; + } + // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted, e.g. request timeout web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) @@ -73,6 +107,21 @@ namespace nmos return config; } + // construct client config based on settings and access token, e.g. using the specified proxy + // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate) + { + auto config = make_websocket_client_config(settings, std::move(load_ca_certificates), gate); + + if (get_authorization_bearer_token) + { + const auto bearer_token = get_authorization_bearer_token(); + config.headers().add(web::http::header_names::authorization, U("Bearer ") + bearer_token.access_token()); + } + + return config; + } + // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) diff --git a/Development/nmos/client_utils.h b/Development/nmos/client_utils.h index 1e76e8bc8..12eb38f03 100644 --- a/Development/nmos/client_utils.h +++ b/Development/nmos/client_utils.h @@ -2,6 +2,7 @@ #define NMOS_CLIENT_UTILS_H #include "cpprest/http_client.h" // for http_client, http_client_config, http_response, etc. +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" #include "nmos/settings.h" @@ -13,10 +14,13 @@ namespace nmos { // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate); web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // make an API request with logging diff --git a/Development/nmos/connection_api.cpp b/Development/nmos/connection_api.cpp index 385a31c76..eb63bcafb 100644 --- a/Development/nmos/connection_api.cpp +++ b/Development/nmos/connection_api.cpp @@ -24,7 +24,7 @@ namespace nmos { web::http::experimental::listener::api_router make_unmounted_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate); - web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate) + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -42,6 +42,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + connection_api.support(U("/x-nmos/") + nmos::patterns::connection_api.pattern + U("/?"), validate_authorization); + connection_api.support(U("/x-nmos/") + nmos::patterns::connection_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is05_versions::from_settings(model.settings); }); connection_api.support(U("/x-nmos/") + nmos::patterns::connection_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -54,9 +60,14 @@ namespace nmos return connection_api; } + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate) + { + return make_connection_api(model, parse_transport_file, validate_merged, {}, gate); + } + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, slog::base_gate& gate) { - return make_connection_api(model, &parse_rtp_transport_file, {}, gate); + return make_connection_api(model, &parse_rtp_transport_file, {}, {}, gate); } inline bool is_connection_api_permitted_downgrade(const nmos::resource& resource, const nmos::resource& connection_resource, const nmos::api_version& version) diff --git a/Development/nmos/connection_api.h b/Development/nmos/connection_api.h index 6a7cd47be..bc6b24182 100644 --- a/Development/nmos/connection_api.h +++ b/Development/nmos/connection_api.h @@ -37,11 +37,11 @@ namespace nmos // Connection API factory functions // callbacks from this function are called with the model locked, and may read but should not write directly to the model - web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate); + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); inline web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, slog::base_gate& gate) { - return make_connection_api(model, std::move(parse_transport_file), {}, gate); + return make_connection_api(model, std::move(parse_transport_file), {}, {}, gate); } web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, slog::base_gate& gate); diff --git a/Development/nmos/connection_events_activation.cpp b/Development/nmos/connection_events_activation.cpp index 17b014332..cafc6e2bf 100644 --- a/Development/nmos/connection_events_activation.cpp +++ b/Development/nmos/connection_events_activation.cpp @@ -11,9 +11,9 @@ namespace nmos { // this handler can be used to (un)subscribe IS-07 Events WebSocket receivers with the specified handlers, when they are activated - nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) + nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::authorization_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate) { - std::shared_ptr events_ws_client(new nmos::events_ws_client(nmos::make_websocket_client_config(settings, load_ca_certificates, gate), nmos::fields::events_heartbeat_interval(settings), gate)); + std::shared_ptr events_ws_client(new nmos::events_ws_client(nmos::make_websocket_client_config(settings, load_ca_certificates, get_authorization_bearer_token, gate), nmos::fields::events_heartbeat_interval(settings), gate)); events_ws_client->set_message_handler(message_handler); events_ws_client->set_close_handler(close_handler); diff --git a/Development/nmos/connection_events_activation.h b/Development/nmos/connection_events_activation.h index 1014f2400..573df6e14 100644 --- a/Development/nmos/connection_events_activation.h +++ b/Development/nmos/connection_events_activation.h @@ -1,6 +1,7 @@ #ifndef NMOS_CONNECTION_EVENTS_ACTIVATION_H #define NMOS_CONNECTION_EVENTS_ACTIVATION_H +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" #include "nmos/connection_activation.h" #include "nmos/events_ws_client.h" // for nmos::events_ws_message_handler, etc. @@ -11,7 +12,12 @@ namespace nmos struct node_model; // this handler can be used to (un)subscribe IS-07 Events WebSocket receivers with the specified handlers, when they are activated - nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::load_ca_certificates_handler load_ca_certificates, nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate); + nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::authorization_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate); + + inline nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::load_ca_certificates_handler load_ca_certificates, nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) + { + return make_connection_events_websocket_activation_handler(load_ca_certificates, std::move(message_handler), std::move(close_handler), {}, settings, gate); + } inline nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) { diff --git a/Development/nmos/events_api.cpp b/Development/nmos/events_api.cpp index 9567f58a7..ccef80e62 100644 --- a/Development/nmos/events_api.cpp +++ b/Development/nmos/events_api.cpp @@ -10,7 +10,7 @@ namespace nmos { web::http::experimental::listener::api_router make_unmounted_events_api(const nmos::node_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_events_api(const nmos::node_model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_events_api(nmos::node_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -28,6 +28,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + events_api.support(U("/x-nmos/") + nmos::patterns::events_api.pattern + U("/?"), validate_authorization); + events_api.support(U("/x-nmos/") + nmos::patterns::events_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is07_versions::from_settings(model.settings); }); events_api.support(U("/x-nmos/") + nmos::patterns::events_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { diff --git a/Development/nmos/events_api.h b/Development/nmos/events_api.h index a6ef568a7..810b6563c 100644 --- a/Development/nmos/events_api.h +++ b/Development/nmos/events_api.h @@ -14,7 +14,12 @@ namespace nmos { struct node_model; - web::http::experimental::listener::api_router make_events_api(const nmos::node_model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_events_api(nmos::node_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_events_api(nmos::node_model& model, slog::base_gate& gate) + { + return make_events_api(model, {}, gate); + } } #endif diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index cd5485e33..d56d61f53 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -3,12 +3,15 @@ #include #include "cpprest/json_storage.h" #include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_state.h" #include "nmos/is07_versions.h" #include "nmos/log_manip.h" #include "nmos/model.h" #include "nmos/query_utils.h" #include "nmos/rational.h" #include "nmos/thread_utils.h" // for wait_until +#include "nmos/scope.h" #include "nmos/slog.h" #include "nmos/version.h" @@ -23,18 +26,83 @@ namespace nmos // by the IS-04 Registration API, so this implementation also shares much commonality. // See nmos/query_ws_api.cpp and nmos/registration_api.cpp - web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, slog::base_gate& gate_) + web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate_) { - return [&model, &gate_](web::http::http_request req) + return [&model, ws_validate_authorization, &gate_](web::http::http_request req) { nmos::ws_api_gate gate(gate_, req.request_uri()); - auto lock = model.read_lock(); + auto lock = model.write_lock(); auto& resources = model.connection_resources; // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket // Clients SHOULD use the "Authorization Request Header Field" method. // Clients MAY use a "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 + if (ws_validate_authorization) + { + if (!ws_validate_authorization(req, nmos::experimental::scopes::events)) { return false; } + } + + // For now, to determine whether the "resource name" is valid, only look at the path, and ignore any query parameters + const auto& ws_resource_path = req.request_uri().path(); + slog::log(gate, SLOG_FLF) << "Validating websocket connection to: " << ws_resource_path; + + const bool has_ws_resource_path = resources.end() != find_resource_if(resources, nmos::types::sender, [&ws_resource_path](const nmos::resource& resource) + { + auto active = nmos::fields::master_enable(nmos::fields::endpoint_active(resource.data)); + auto& transport_params = nmos::fields::transport_params(nmos::fields::endpoint_active(resource.data)); + auto& connection_uri = nmos::fields::connection_uri(transport_params.at(0)); + return active + && !connection_uri.is_null() + && ws_resource_path == web::uri(connection_uri.as_string()).path(); + }); + + if (!has_ws_resource_path) slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << ws_resource_path; + return has_ws_resource_path; + }; + } + + web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::authorization_state& authorization_state, slog::base_gate& gate_) + { + return [&model, &authorization_state, &gate_](web::http::http_request req) + { + nmos::ws_api_gate gate(gate_, req.request_uri()); + auto lock = model.write_lock(); + auto& resources = model.connection_resources; + + // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket + // Clients SHOULD use the "Authorization Request Header Field" method. + // Clients MAY use a "URI Query Parameter". + // See https://tools.ietf.org/html/rfc6750#section-2 + if (web::http::methods::OPTIONS != req.method() && nmos::experimental::fields::server_authorization(model.settings)) + { + const auto& settings = model.settings; + + authorization_state.write_lock(); + const auto error = nmos::experimental::ws_validate_authorization(authorization_state.issuers, req, nmos::experimental::scopes::events, nmos::get_host_name(settings), authorization_state.authorization_server_uri, authorization_state.token_issuer, gate_); + if (error) + { + // set error repsonse + auto realm = web::http::get_host_port(req).first; + if (realm.empty()) { realm = nmos::get_host(settings); } + web::http::http_response res; + nmos::experimental::details::set_error_reply(res, realm, error); + req.reply(res); + + // if error was deal to no matching keys, trigger authorization_token_issuer_thread to fetch public keys from the token issuer + if (error.value == nmos::experimental::authorization_error::no_matching_keys) + { + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << error.message; + authorization_state.fetch_token_issuer_pubkeys = true; + model.notify(); + } + else + { + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << error.message; + } + return false; + } + } // For now, to determine whether the "resource name" is valid, only look at the path, and ignore any query parameters const auto& ws_resource_path = req.request_uri().path(); diff --git a/Development/nmos/events_ws_api.h b/Development/nmos/events_ws_api.h index 74a9e603e..09f12679c 100644 --- a/Development/nmos/events_ws_api.h +++ b/Development/nmos/events_ws_api.h @@ -1,8 +1,10 @@ #ifndef NMOS_EVENTS_WS_API_H #define NMOS_EVENTS_WS_API_H +#include "nmos/authorization_handlers.h" #include "nmos/events_resources.h" #include "nmos/websockets.h" +#include "nmos/ws_api_utils.h" namespace slog { @@ -15,15 +17,15 @@ namespace nmos { struct node_model; - web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, slog::base_gate& gate); + web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate); web::websockets::experimental::listener::open_handler make_events_ws_open_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); web::websockets::experimental::listener::close_handler make_events_ws_close_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); web::websockets::experimental::listener::message_handler make_events_ws_message_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); - inline web::websockets::experimental::listener::websocket_listener_handlers make_events_ws_api(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate) + inline web::websockets::experimental::listener::websocket_listener_handlers make_events_ws_api(nmos::node_model& model, nmos::websockets& websockets, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate) { return{ - nmos::make_events_ws_validate_handler(model, gate), + nmos::make_events_ws_validate_handler(model, ws_validate_authorization, gate), nmos::make_events_ws_open_handler(model, websockets, gate), nmos::make_events_ws_close_handler(model, websockets, gate), nmos::make_events_ws_message_handler(model, websockets, gate) diff --git a/Development/nmos/is10_schemas/is10_schemas.h b/Development/nmos/is10_schemas/is10_schemas.h new file mode 100644 index 000000000..a250b5631 --- /dev/null +++ b/Development/nmos/is10_schemas/is10_schemas.h @@ -0,0 +1,27 @@ +#ifndef NMOS_IS10_SCHEMAS_H +#define NMOS_IS10_SCHEMAS_H + +// Extern declarations for auto-generated constants +// could be auto-generated, but isn't currently! +namespace nmos +{ + namespace is10_schemas + { + namespace v1_0_x + { + extern const char* auth_clients_schema; + + extern const char* auth_metadata; + extern const char* jwks_response; + extern const char* jwks_schema; + extern const char* register_client_error_response; + extern const char* register_client_request; + extern const char* register_client_response; + extern const char* token_error_response; + extern const char* token_response; + extern const char* token_schema; + } + } +} + +#endif diff --git a/Development/nmos/is10_versions.h b/Development/nmos/is10_versions.h new file mode 100644 index 000000000..89e991afc --- /dev/null +++ b/Development/nmos/is10_versions.h @@ -0,0 +1,26 @@ +#ifndef NMOS_IS10_VERSIONS_H +#define NMOS_IS10_VERSIONS_H + +#include +#include +#include "nmos/api_version.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace is10_versions + { + const api_version v1_0{ 1, 0 }; + + const std::set all{ nmos::is10_versions::v1_0 }; + + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::fields::is10_versions) + ? boost::copy_range>(nmos::fields::is10_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + : nmos::is10_versions::all; + } + } +} + +#endif diff --git a/Development/nmos/issuers.h b/Development/nmos/issuers.h new file mode 100644 index 000000000..bc1002462 --- /dev/null +++ b/Development/nmos/issuers.h @@ -0,0 +1,25 @@ +#ifndef NMOS_ISSUERS_H +#define NMOS_ISSUERS_H + +#include "cpprest/json.h" +#include "nmos/jwt_validator.h" + +namespace nmos +{ + namespace experimental + { + struct issuer + { + web::json::value settings; // [U("authorization_server_metadata")], [U("jwks")], [U("client_metadata")], + // where: + // "authorization_server_metadata": issuer (authorization server) metadata + // "jwks": issuer jwks + // "client_metadata": client (Node/Registry) metadata + nmos::experimental::jwt_validator jwt_validator; + }; + + typedef std::map issuers; // where uri: issuer (authorization server) uri + } +} + +#endif diff --git a/Development/nmos/json_fields.h b/Development/nmos/json_fields.h index d04d57923..a637cb751 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -238,6 +238,70 @@ namespace nmos const web::json::field_as_string st2110_21_sender_type{ U("st2110_21_sender_type") }; // see nmos::st2110_21_sender_type } + // IS-10 Authorization + namespace experimental + { + namespace fields + { + // Authorization Server Metadata + const web::json::field_as_value authorization_server_metadata{ U("authorization_server_metadata") }; + // see https://tools.ietf.org/html/rfc8414#section-2 + const web::json::field_as_string_or issuer{ U("issuer"),{} }; + const web::json::field_as_string_or authorization_endpoint{ U("authorization_endpoint"),{} }; + const web::json::field_as_string_or token_endpoint{ U("token_endpoint"),{} }; + const web::json::field_as_string_or registration_endpoint{ U("registration_endpoint"),{} }; + const web::json::field_as_array scopes_supported{ U("scopes_supported") }; // OPTIONAL + const web::json::field_as_array response_types_supported{ U("response_types_supported") }; + const web::json::field_as_array response_modes_supported{ U("response_modes_supported") }; // OPTIONAL + const web::json::field_as_array grant_types_supported{ U("grant_types_supported") }; // OPTIONAL + const web::json::field_as_array token_endpoint_auth_methods_supported{ U("token_endpoint_auth_methods_supported") }; // OPTIONAL + const web::json::field_as_array token_endpoint_auth_signing_alg_values_supported{ U("token_endpoint_auth_signing_alg_values_supported") }; // OPTIONAL + const web::json::field_as_string service_documentation{ U("service_documentation") }; // OPTIONAL + const web::json::field_as_array ui_locales_supported{ U("ui_locales_supported") }; // OPTIONAL + const web::json::field_as_string op_policy_uri{ U("op_policy_uri") }; // OPTIONAL + const web::json::field_as_string op_tos_uri{ U("op_tos_uri") }; // OPTIONAL + const web::json::field_as_string revocation_endpoint{ U("revocation_endpoint") }; // OPTIONAL + const web::json::field_as_array revocation_endpoint_auth_methods_supported{ U("revocation_endpoint_auth_methods_supported") }; // OPTIONAL + const web::json::field_as_array revocation_endpoint_auth_signing_alg_values_supported{ U("revocation_endpoint_auth_signing_alg_values_supported") }; // OPTIONAL + const web::json::field_as_string introspection_endpoint{ U("introspection_endpoint") }; // OPTIONAL + const web::json::field_as_array introspection_endpoint_auth_methods_supported{ U("introspection_endpoint_auth_methods_supported") }; // OPTIONAL + const web::json::field_as_array introspection_endpoint_auth_signing_alg_values_supported{ U("introspection_endpoint_auth_signing_alg_values_supported") }; // OPTIONAL + const web::json::field_as_array code_challenge_methods_supported{ U("code_challenge_methods_supported") }; + + // Client Metadata + const web::json::field_as_value client_metadata{ U("client_metadata") }; + // see https://tools.ietf.org/html/rfc7591#section-2 + // see https://tools.ietf.org/html/rfc7591#section-3.1 + // see https://tools.ietf.org/html/rfc7591#section-3.2 + const web::json::field_as_array redirect_uris{ U("redirect_uris") }; + //const web::json::field_as_string token_endpoint_auth_method{ U("token_endpoint_auth_method") }; // OPTIONAL already defined in settings + const web::json::field_as_array grant_types{ U("grant_types") }; // OPTIONAL + const web::json::field_as_array response_types{ U("response_types") }; // OPTIONAL + const web::json::field_as_string client_name{ U("client_name") }; // OPTIONAL + const web::json::field_as_string client_uri{ U("client_uri") }; // OPTIONAL + const web::json::field_as_string logo_uri{ U("logo_uri") }; // OPTIONAL + const web::json::field_as_string scope{ U("scope") }; // OPTIONAL + const web::json::field_as_array contacts{ U("contacts") }; // OPTIONAL + const web::json::field_as_string tos_uri{ U("tos_uri") }; // OPTIONAL + const web::json::field_as_string policy_uri{ U("policy_uri") }; // OPTIONAL + const web::json::field_as_value jwks{ U("jwks") }; // OPTIONAL + const web::json::field_as_array keys{ U("keys") }; // use inside jwks + const web::json::field_as_string software_id{ U("software_id") }; // OPTIONAL + const web::json::field_as_string software_version{ U("software_version") }; // OPTIONAL + const web::json::field_as_string_or client_id{ U("client_id"),{} }; + const web::json::field_as_string client_secret{ U("client_secret") }; // OPTIONAL + const web::json::field_as_integer client_id_issued_at{ U("client_id_issued_at") }; // OPTIONAL + const web::json::field_as_integer_or client_secret_expires_at{ U("client_secret_expires_at"),0 }; + // OpenID Connect extension + const web::json::field_as_string registration_client_uri{ U("registration_client_uri") }; // OPTIONAL + const web::json::field_as_string registration_access_token{ U("registration_access_token") }; // OPTIONAL + + // use for Authorization Server Metadata & Client Metadata + const web::json::field_as_string_or jwks_uri{ U("jwks_uri"),{} }; + } + } + + // Fields for experimental extensions namespace experimental { diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index fdd70581d..f59ae42db 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -9,6 +9,7 @@ #include "nmos/is08_schemas/is08_schemas.h" #include "nmos/is09_versions.h" #include "nmos/is09_schemas/is09_schemas.h" +#include "nmos/is10_schemas/is10_schemas.h" #include "nmos/type.h" namespace nmos @@ -126,6 +127,29 @@ namespace nmos const web::uri systemapi_global_schema_uri = make_schema_uri(tag, _XPLATSTR("global.json")); } } + + namespace is10_schemas + { + web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) + { + return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-authorization/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + } + + namespace v1_0 + { + using namespace nmos::is10_schemas::v1_0_x; + const utility::string_t tag(_XPLATSTR("v1.0.x")); + + const web::uri auth_clients_schema_uri = make_schema_uri(tag, _XPLATSTR("auth_clients_schema.json")); + + const web::uri authapi_auth_metadata_schema_uri = make_schema_uri(tag, _XPLATSTR("auth_metadata.json")); + const web::uri authapi_jwks_response_schema_uri = make_schema_uri(tag, _XPLATSTR("jwks_response.json")); + const web::uri authapi_register_client_error_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_error_response.json")); + const web::uri authapi_register_client_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_response.json")); + const web::uri authapi_token_response_schema_uri = make_schema_uri(tag, _XPLATSTR("token_response.json")); + const web::uri authapi_token_schema_schema_uri = make_schema_uri(tag, _XPLATSTR("token_schema.json")); + } + } } namespace nmos @@ -310,6 +334,25 @@ namespace nmos }; } + static std::map make_is10_schemas() + { + using namespace nmos::is10_schemas; + + return + { + // v1.0 + { make_schema_uri(v1_0::tag, _XPLATSTR("auth_clients_schema.json")), make_schema(v1_0::auth_clients_schema) }, + + { make_schema_uri(v1_0::tag, _XPLATSTR("auth_metadata.json")), make_schema(v1_0::auth_metadata) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_response.json")), make_schema(v1_0::jwks_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_schema.json")), make_schema(v1_0::jwks_schema) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("register_client_error_response.json")), make_schema(v1_0::register_client_error_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("register_client_response.json")), make_schema(v1_0::register_client_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("token_response.json")), make_schema(v1_0::token_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("token_schema.json")), make_schema(v1_0::token_schema) } + }; + } + inline void merge(std::map& to, std::map&& from) { to.insert(from.begin(), from.end()); // std::map::merge in C++17 @@ -321,6 +364,7 @@ namespace nmos merge(result, make_is05_schemas()); merge(result, make_is08_schemas()); merge(result, make_is09_schemas()); + merge(result, make_is10_schemas()); return result; } @@ -382,6 +426,36 @@ namespace nmos return is08_schemas::v1_0::map_activations_post_request_uri; } + web::uri make_auth_clients_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::auth_clients_schema_uri; + } + + web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_auth_metadata_schema_uri; + } + + web::uri make_authapi_jwks_response_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_jwks_response_schema_uri; + } + + web::uri make_authapi_register_client_response_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_register_client_response_uri; + } + + web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_token_schema_schema_uri; + } + + web::uri make_authapi_token_response_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_token_response_schema_uri; + } + // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id) { diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index e938a513e..d71ae9940 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -29,6 +29,14 @@ namespace nmos web::uri make_channelmappingapi_map_activations_post_request_schema_uri(const nmos::api_version& version); + web::uri make_auth_clients_schema_uri(const nmos::api_version& version); + + web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version); + web::uri make_authapi_jwks_response_schema_uri(const nmos::api_version& version); + web::uri make_authapi_register_client_response_uri(const nmos::api_version& version); + web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version); + web::uri make_authapi_token_response_schema_uri(const nmos::api_version& version); + // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id); } diff --git a/Development/nmos/jwk_utils.cpp b/Development/nmos/jwk_utils.cpp new file mode 100644 index 000000000..f56591c25 --- /dev/null +++ b/Development/nmos/jwk_utils.cpp @@ -0,0 +1,387 @@ +#include "nmos/jwk_utils.h" + +#include +#include +#include +#include +#include +#include "cpprest/basic_utils.h" +#include "ssl/ssl_utils.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + typedef std::unique_ptr BIGNUM_ptr; + typedef std::unique_ptr RSA_ptr; + typedef std::unique_ptr EC_KEY_ptr; + typedef std::unique_ptr EVP_PKEY_ptr; + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + int RSA_set0_key(RSA* r, BIGNUM* n, BIGNUM* e, BIGNUM* d) + { + /* If the fields n and e in r are NULL, the corresponding input + * parameters MUST be non-NULL for n and e. d may be + * left NULL (in case only the public key is used). + */ + if ((r->n == NULL && n == NULL) + || (r->e == NULL && e == NULL)) + return 0; + + if (n != NULL) { + BN_free(r->n); + r->n = n; + } + if (e != NULL) { + BN_free(r->e); + r->e = e; + } + if (d != NULL) { + BN_free(r->d); + r->d = d; + } + + return 1; + } + + void RSA_get0_key(const RSA* r, const BIGNUM** n, const BIGNUM** e, const BIGNUM** d) + { + if (n != NULL) + *n = r->n; + if (e != NULL) + *e = r->e; + if (d != NULL) + *d = r->d; + } +#endif + // convert JSON Web Key to RSA Public Key + // The "n" (modulus) parameter contains the modulus value for the RSA public key + // It is represented as a Base64urlUInt - encoded value + // The "e" (exponent)parameter contains the exponent value for the RSA public key + // It is represented as a Base64urlUInt - encoded value + // see https://tools.ietf.org/html/rfc7518#section-6.3.1 + // this function is based on https://stackoverflow.com/questions/57217529/how-to-convert-jwk-public-key-to-pem-format-in-c + utility::string_t jwk_to_public_key(const utility::string_t& base64_n, const utility::string_t& base64_e) + { + using ssl::experimental::BIO_ptr; + + auto n = utility::conversions::from_base64url(base64_n); + auto e = utility::conversions::from_base64url(base64_e); + + BIGNUM_ptr modulus(BN_bin2bn(n.data(), (int)n.size(), NULL), &BN_free); + BIGNUM_ptr exponent(BN_bin2bn(e.data(), (int)e.size(), NULL), &BN_free); + + RSA_ptr rsa(RSA_new(), &RSA_free); + if (!rsa) + { + throw jwk_exception("convert jwk to pem error: failed to create RSA"); + } + + // "Calling this function transfers the memory management of the values to the RSA object, + // and therefore the values that have been passed in should not be freed by the caller after + // this function has been called." + // see https://www.openssl.org/docs/man1.1.1/man3/RSA_set0_key.html + if (RSA_set0_key(rsa.get(), modulus.get(), exponent.get(), NULL)) + { + modulus.release(); + exponent.release(); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to initialise RSA"); + } + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert jwk to pem error: failed to create BIO memory"); + } + if (PEM_write_bio_RSA_PUBKEY(bio.get(), rsa.get())) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()); + return utility::s2us(pem); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to write BIO to pem"); + } + } + + // convert JSON Web Key to EC Public Key + // The "crv" (curve) parameter identifies the cryptographic curve used with the EC public key + // The supported curves are "P-256", "P-384" and "P-521" + // The "x" (x coordinate) parameter contains the x coordinate for the Elliptic Curve point + // It is represented as the base64url encoding of the octet string representation of the coordinate + // The "y" (y coordinate) parameter contains the y coordinate for the Elliptic Curve point + // It is represented as the base64url encoding of the octet string representation of the coordinate + // see https://tools.ietf.org/html/rfc7518#section-6.2.1 + utility::string_t jwk_to_public_key(const utility::string_t& curve_type, const utility::string_t& base64_x, const utility::string_t& base64_y) + { + using ssl::experimental::BIO_ptr; + + // supported Elliptic-Curve types + // see https://tools.ietf.org/search/rfc4492#appendix-A + const std::map curve = + { + { U("P-256"), NID_X9_62_prime256v1 }, + { U("P-384"), NID_secp384r1 }, + { U("P-521"), NID_secp521r1 } + }; + + auto found = curve.find(curve_type); + if (curve.end() == found) + { + throw jwk_exception("convert jwk to pem error: EC type not supported"); + } + EC_KEY_ptr ec_key(EC_KEY_new_by_curve_name(found->second), &EC_KEY_free); + if (!ec_key) + { + throw jwk_exception("convert jwk to pem error: failed to create EC key with named curve"); + } + + auto x = utility::conversions::from_base64url(base64_x); + auto y = utility::conversions::from_base64url(base64_y); + + BIGNUM_ptr x_coordinate(BN_bin2bn(x.data(), (int)x.size(), NULL), &BN_free); + BIGNUM_ptr y_coordinate(BN_bin2bn(y.data(), (int)y.size(), NULL), &BN_free); + + if (EC_KEY_set_public_key_affine_coordinates(ec_key.get(), x_coordinate.get(), y_coordinate.get())) + { + x_coordinate.release(); + y_coordinate.release(); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to initialise EC"); + } + + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert jwk to pem error: failed to create BIO memory"); + } + if (PEM_write_bio_EC_PUBKEY(bio.get(), ec_key.get())) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()); + return utility::s2us(pem); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to write BIO to pem"); + } + } + + // convert JSON Web Key to public key in pem format + utility::string_t jwk_to_public_key(const web::json::value& jwk) + { + // Key Type (kty) + // see https://tools.ietf.org/html/rfc7517#section-4.1 + + // RSA Public Keys + // see https://tools.ietf.org/html/rfc7518#section-6.3.1 + if (U("RSA") == jwk.at(U("kty")).as_string()) + { + // Public Key Use (use), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.2 + if (jwk.has_field(U("use"))) + { + if (U("sig") != jwk.at(U("use")).as_string()) throw jwk_exception("jwk contains invalid 'use': " + utility::us2s(jwk.serialize())); + } + + // is n presented? + // Base64 URL encoded string representing the modulus of the RSA Key + // see https://tools.ietf.org/html/rfc7518#section-6.3.1.1 + if (!jwk.has_field(U("n"))) throw jwk_exception("jwk does not contain 'n': " + utility::us2s(jwk.serialize())); + + // is e presented? + // Base64 URL encoded string representing the public exponent of the RSA Key + // see https://tools.ietf.org/html/rfc7518#section-6.3.1.2 + if (!jwk.has_field(U("e"))) throw jwk_exception("jwk does not contain 'e': " + utility::us2s(jwk.serialize())); + + // using n & e to convert Json Web Key to RSA Public Key + return jwk_to_public_key(jwk.at(U("n")).as_string(), jwk.at(U("e")).as_string()); // may throw jwk_exception + } + // Elliptic Curve Public Keys + // see https://tools.ietf.org/html/rfc7518#section-6.2 + else if (U("EC") == jwk.at(U("kty")).as_string()) + { + // Public Key Use (use), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.2 + if (jwk.has_field(U("use"))) + { + if (U("sig") != jwk.at(U("use")).as_string()) throw jwk_exception("jwk contains invalid 'use': " + utility::us2s(jwk.serialize())); + } + + // is crv presented? + // The "crv" (curve) parameter identifies the cryptographic curve used with the EC public Key + // see https://tools.ietf.org/html/rfc7518#section-6.2.1.1 + if (!jwk.has_field(U("crv"))) throw jwk_exception("jwk does not contain 'crv': " + utility::us2s(jwk.serialize())); + + // is x presented? + // Base64 URL encoded string representation of the x coordinate of the EC public Key + // see https://tools.ietf.org/html/rfc7518#section-6.2.1.2 + if (!jwk.has_field(U("x"))) throw jwk_exception("jwk does not contains 'x': " + utility::us2s(jwk.serialize())); + + // is y presented? + // Base64 URL encoded string representation of the y coordinate of the EC public Key + // see https://tools.ietf.org/html/rfc7518#section-6.2.1.3 + if (!jwk.has_field(U("y"))) throw jwk_exception("jwk does not contain 'y': " + utility::us2s(jwk.serialize())); + + // using crv, x & y to convert Json Web Key to EC Public Key + return jwk_to_public_key(jwk.at(U("crv")).as_string(), jwk.at(U("x")).as_string(), jwk.at(U("y")).as_string()); // may throw jwk_exception + } + else + { + throw jwk_exception("jwk contains invalid 'kty': " + utility::us2s(jwk.serialize())); + } + } + + // convert RSA to JSON Web Key + web::json::value rsa_to_jwk(const RSA_ptr& rsa, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use, const jwk::algorithm& alg) + { + const BIGNUM* modulus = NULL; + const BIGNUM* exponent = NULL; + + // The n, e and d parameters can be obtained by calling RSA_get0_key(). + // If they have not been set yet, then *n, *e and *d will be set to NULL. + // Otherwise, they are set to pointers to their respective values. + //These point directly to the internal representations of the values and + // therefore should not be freed by the caller. + // see https://manpages.debian.org/unstable/libssl-doc/RSA_get0_key.3ssl.en.html#DESCRIPTION + RSA_get0_key(rsa.get(), &modulus, &exponent, NULL); + + const auto modulus_bytes = BN_num_bytes(modulus); + std::vector n(modulus_bytes); + BN_bn2bin(modulus, n.data()); + const auto base64_n = utility::conversions::to_base64url(n); + + const auto exponent_bytes = BN_num_bytes(exponent); + std::vector e(exponent_bytes); + BN_bn2bin(exponent, e.data()); + const auto base64_e = utility::conversions::to_base64url(e); + + // construct jwk + return web::json::value_of({ + { U("kid"), keyid }, + { U("kty"), U("RSA") }, + { U("n"), base64_n }, + { U("e"), base64_e }, + { U("alg"), alg.name }, + { U("use"), pubkey_use.name } + }); + } + + // convert RSA public key to JSON Web Key + web::json::value public_key_to_jwk(const utility::string_t& pubkey, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use, const jwk::algorithm& alg) + { + using ssl::experimental::BIO_ptr; + + const std::string public_key{ utility::us2s(pubkey) }; + BIO_ptr bio(BIO_new_mem_buf((void*)public_key.c_str(), (int)public_key.length()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert pem to jwk error: failed to create BIO memory from public key"); + } + + RSA* rsa_ = NULL; + RSA_ptr rsa(PEM_read_bio_RSA_PUBKEY(bio.get(), &rsa_, NULL, NULL), &RSA_free); + if(!rsa) + { + throw jwk_exception("convert pem to jwk error: failed to load RSA"); + } + + return rsa_to_jwk(rsa, keyid, pubkey_use, alg); + } + + // convert RSA private key to JSON Web Key + web::json::value private_key_to_jwk(const utility::string_t& private_key_, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use, const jwk::algorithm& alg) + { + using ssl::experimental::BIO_ptr; + + const std::string buffer{ utility::us2s(private_key_) }; + BIO_ptr bio(BIO_new_mem_buf((void*)buffer.c_str(), (int)buffer.length()), &BIO_free); + EVP_PKEY_ptr private_key(PEM_read_bio_PrivateKey(bio.get(), NULL, NULL, NULL), &EVP_PKEY_free); + + if (private_key) + { + RSA_ptr rsa(EVP_PKEY_get1_RSA(private_key.get()), &RSA_free); + if (rsa) + { + return rsa_to_jwk(rsa, keyid, pubkey_use, alg); + } + } + return{}; + } + + // find the RSA private key from private key list + utility::string_t found_rsa_key(const std::vector& private_keys) + { + using ssl::experimental::BIO_ptr; + + for (const auto private_key_ : private_keys) + { + const std::string buffer{ utility::us2s(private_key_) }; + BIO_ptr bio(BIO_new_mem_buf((void*)buffer.c_str(), (int)buffer.length()), &BIO_free); + EVP_PKEY_ptr private_key(PEM_read_bio_PrivateKey(bio.get(), NULL, NULL, NULL), &EVP_PKEY_free); + + if (private_key) + { + RSA_ptr rsa(EVP_PKEY_get1_RSA(private_key.get()), &RSA_free); + if (rsa) + { + return private_key_; + } + } + } + return{}; + } + + // extract RSA public key from RSA private key + utility::string_t rsa_public_key(const utility::string_t& private_key_) + { + using ssl::experimental::BIO_ptr; + + const std::string private_key_buffer{ utility::us2s(private_key_) }; + BIO_ptr private_key_bio(BIO_new_mem_buf((void*)private_key_buffer.c_str(), (int)private_key_buffer.length()), &BIO_free); + if (!private_key_bio) + { + throw jwk_exception("extract public key error: failed to create BIO memory from private key"); + } + + EVP_PKEY_ptr private_key(PEM_read_bio_PrivateKey(private_key_bio.get(), NULL, NULL, NULL), &EVP_PKEY_free); + + if (!private_key) + { + throw jwk_exception("extract public key error: failed to read BIO private key"); + } + + RSA_ptr rsa(EVP_PKEY_get1_RSA(private_key.get()), &RSA_free); + if (!rsa) + { + throw jwk_exception("extract public key error: failed to load RSA key"); + } + + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (bio && PEM_write_bio_RSA_PUBKEY(bio.get(), rsa.get())) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string public_key(size_t(buf->length), 0); + BIO_read(bio.get(), (void*)public_key.data(), (int)public_key.length()); + return utility::s2us(public_key); + } + else + { + throw jwk_exception("extract public key error: failed to read RSA public key"); + } + } + } + } +} diff --git a/Development/nmos/jwk_utils.h b/Development/nmos/jwk_utils.h new file mode 100644 index 000000000..6fd89dfa2 --- /dev/null +++ b/Development/nmos/jwk_utils.h @@ -0,0 +1,37 @@ +#ifndef NMOS_JWK_UTILS_H +#define NMOS_JWK_UTILS_H + +#include "cpprest/json_utils.h" +#include "jwk/algorithm.h" +#include "jwk/public_key_use.h" + +namespace nmos +{ + namespace experimental + { + struct jwk_exception : std::runtime_error + { + jwk_exception(const std::string& message) : std::runtime_error(message) {} + }; + + namespace details + { + // convert JSON Web Key to public key + utility::string_t jwk_to_public_key(const web::json::value& jwk); + + // convert public key to JSON Web Key (RSA only) + web::json::value public_key_to_jwk(const utility::string_t& pubkey, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use = jwk::public_key_uses::signing, const jwk::algorithm& alg = jwk::algorithms::RS256); + + // convert RSA private key to JSON Web Key + web::json::value private_key_to_jwk(const utility::string_t& private_key, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use = jwk::public_key_uses::signing, const jwk::algorithm& alg = jwk::algorithms::RS256); + + // find the RSA private key from private key list + utility::string_t found_rsa_key(const std::vector& private_keys); + + // extract RSA public key from RSA private key + utility::string_t rsa_public_key(const utility::string_t& private_key); + } + } +} + +#endif diff --git a/Development/nmos/jwks_uri_api.cpp b/Development/nmos/jwks_uri_api.cpp new file mode 100644 index 000000000..bd1b0a7b2 --- /dev/null +++ b/Development/nmos/jwks_uri_api.cpp @@ -0,0 +1,62 @@ +#include "nmos/jwks_uri_api.h" + +#include "cpprest/response_type.h" +#include "nmos/api_utils.h" +#include "nmos/authorization_utils.h" +#include "nmos/jwk_utils.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + web::http::experimental::listener::api_router make_jwk_uri_api(nmos::base_model& model, load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& /*gate_*/) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router jwks_api; + + jwks_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-authorization/") }, req, res)); + return pplx::task_from_result(true); + }); + + jwks_api.support(U("/x-authorization/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("callback/"), U("jwks/") }, req, res)); + return pplx::task_from_result(true); + }); + + jwks_api.support(U("/x-authorization/jwks/?"), methods::GET, [&model, load_rsa_private_keys](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + using web::json::array; + + // hmm, for now new "kid" key ID is used for every request, may be it should only be updated after key has changed + + auto keys = value::array(); + std::vector rsa_private_keys; + with_read_lock(model.mutex, [&model, &rsa_private_keys, load_rsa_private_keys] + { + rsa_private_keys = load_rsa_private_keys(); + }); + + for (const auto rsa_private_key : rsa_private_keys) + { + const auto jwk = details::private_key_to_jwk(rsa_private_key, make_id()); + web::json::push_back(keys, jwk); + } + + const auto jwks = value_of({ + { nmos::experimental::fields::keys, keys } + }); + + set_reply(res, status_codes::OK, jwks); + return pplx::task_from_result(true); + }); + + return jwks_api; + } + } +} diff --git a/Development/nmos/jwks_uri_api.h b/Development/nmos/jwks_uri_api.h new file mode 100644 index 000000000..b2e7d39cf --- /dev/null +++ b/Development/nmos/jwks_uri_api.h @@ -0,0 +1,23 @@ +#ifndef NMOS_JWK_URI_API_H +#define NMOS_JWK_URI_API_H + +#include "cpprest/api_router.h" +#include "nmos/certificate_handlers.h" + +namespace slog +{ + class base_gate; +} + +// This is an experimental extension to support authorization code via a REST API +namespace nmos +{ + struct base_model; + + namespace experimental + { + web::http::experimental::listener::api_router make_jwk_uri_api(nmos::base_model& model, load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/jwt_generator.h b/Development/nmos/jwt_generator.h new file mode 100644 index 000000000..71b27902e --- /dev/null +++ b/Development/nmos/jwt_generator.h @@ -0,0 +1,18 @@ +#ifndef NMOS_JWT_GENERATOR_H +#define NMOS_JWT_GENERATOR_H + +#include "cpprest/base_uri.h" + +namespace nmos +{ + namespace experimental + { + class jwt_generator + { + public: + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key); + }; + } +} + +#endif diff --git a/Development/nmos/jwt_generator_impl.cpp b/Development/nmos/jwt_generator_impl.cpp new file mode 100644 index 000000000..154e5d7e5 --- /dev/null +++ b/Development/nmos/jwt_generator_impl.cpp @@ -0,0 +1,55 @@ +#include "nmos/jwt_generator.h" + +#include "cpprest/basic_utils.h" +#include "jwt/nlohmann_traits.h" +#include "nmos/id.h" +#include "nmos/jwk_utils.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + class jwt_generator_impl + { + public: + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& public_key, const utility::string_t& private_key) + { + using namespace jwt::experimental::details; + + // use server private key to create client_assertion (JWT) + // where client_assertion MUST including iss, sub, aud, exp, and may including jti + // see https://tools.ietf.org/html/rfc7523#section-2.2 + // see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + return utility::s2us(jwt::create() + .set_issuer(utility::us2s(issuer)) + .set_subject(utility::us2s(subject)) + .set_audience(utility::us2s(audience.to_string())) + .set_issued_at(std::chrono::system_clock::now()) + .set_expires_at(std::chrono::system_clock::now() + token_lifetime) + .set_id(utility::us2s(nmos::make_id())) + .set_type("JWT") + .sign(jwt::algorithm::rs256(utility::us2s(public_key), utility::us2s(private_key)))); + } + + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key) + { + return create_client_assertion(issuer, subject, audience, token_lifetime, rsa_public_key(private_key), private_key); + } + }; + } + + utility::string_t jwt_generator::create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key) + { + try + { + return details::jwt_generator_impl::create_client_assertion(issuer, subject, audience, token_lifetime, private_key); + } + catch (const jwt::error::signature_generation_exception& e) + { + throw std::invalid_argument(e.what()); + } + } + } +} diff --git a/Development/nmos/jwt_validator.h b/Development/nmos/jwt_validator.h new file mode 100644 index 000000000..9aec9afe0 --- /dev/null +++ b/Development/nmos/jwt_validator.h @@ -0,0 +1,65 @@ +#ifndef NMOS_JWT_VALIDATOR_H +#define NMOS_JWT_VALIDATOR_H + +#include "cpprest/base_uri.h" + +namespace web +{ + namespace json + { + class value; + } + namespace http + { + class http_request; + } +} + +namespace nmos +{ + namespace experimental + { + struct insufficient_scope_exception : std::runtime_error + { + insufficient_scope_exception(const std::string& message) : std::runtime_error(message) {} + }; + + struct no_matching_keys_exception : std::runtime_error + { + web::uri issuer; + no_matching_keys_exception(const web::uri& issuer, const std::string& message) + : std::runtime_error(message) + , issuer(issuer) {} + }; + + struct scope; + + namespace details + { + class jwt_validator_impl; + } + + // callback for validating bearer_token + typedef std::function token_validator; + + class jwt_validator + { + public: + jwt_validator() {} + jwt_validator(const web::json::value& pub_keys, token_validator token_validation); + + bool is_initialized() const; + + void validate_expiry(const utility::string_t& token) const; + void validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server) const; + + static utility::string_t client_id(const utility::string_t& token); + static web::uri token_issuer(const utility::string_t& token); + + private: + std::shared_ptr impl; + }; + } +} + +#endif diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp new file mode 100644 index 000000000..df476b210 --- /dev/null +++ b/Development/nmos/jwt_validator_impl.cpp @@ -0,0 +1,494 @@ +#include "nmos/jwt_validator.h" + +#include +#include "cpprest/basic_utils.h" +#include "cpprest/http_msg.h" +#include "cpprest/json.h" +#include "cpprest/regex_utils.h" +#include "cpprest/uri_schemes.h" +#include "jwt/nlohmann_traits.h" +#include "nmos/authorization_utils.h" +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + class jwt_validator_impl + { + public: + explicit jwt_validator_impl(const web::json::value& pubkeys, token_validator token_validation) + : token_validation(token_validation) + { + using namespace jwt::experimental::details; + + if (pubkeys.is_array()) + { + // empty out all jwt validators + validators.clear(); + + // create jwt verifier for each public key + + // preload JWT verifiers with authorization server publc keys (pems), should perform faster on token validation rather than load the public key then validation at runtime + + // "The access token MUST be a JSON Web Signature (JWS) as defined by RFC 7515. JSON Web Algorithms (JWA) MUST NOT be used. + // The JWS MUST be signed with RSASSA-PKCS1-v1_5 using SHA-512, meaning the value of the alg field in the token's JOSE (JSON Object Signing and Encryption) header (see RFC 7515) + // MUST be set to RS512 as defined in RFC 7518." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#behaviour-access-tokens + for (const auto& pubkey : pubkeys.as_array()) + { + const auto& jwk = pubkey.at(U("jwk")); + + // Key Type (kty) + // see https://tools.ietf.org/html/rfc7517#section-4.1 + if (U("RSA") != jwk.at(U("kty")).as_string()) continue; + + // Public Key Use (use), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.2 + if (jwk.has_field(U("use"))) + { + if (U("sig") != jwk.at(U("use")).as_string()) continue; + } + + // Algorithm (alg), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.4 + if (jwk.has_field(U("alg"))) + { + if (U("RS512") != jwk.at(U("alg")).as_string()) continue; + } + + auto validator = jwt::verify({}); + try + { + validator.allow_algorithm(jwt::algorithm::rs512(utility::us2s(pubkey.at(U("pem")).as_string()))); + validators.push_back(validator); + } + catch (const jwt::rsa_exception&) + { + // hmm, maybe log the error? + } + } + } + } + + void validate_expiry(const utility::string_t& token) const + { + using namespace jwt::experimental::details; + + // verify JWT is well formed + auto decoded_token = jwt::decode(utility::us2s(token)); + + for (const auto& validator : validators) + { + try + { + // verify the signature & some common claims, such as exp, iat, nbf etc + validator.verify(decoded_token); + + // token not expired + return; + } + catch (const jwt::error::signature_verification_exception&) + { + // ignore, try next validator + } + } + + // no public keys to validate access token + throw std::runtime_error("no public keys to validate access token expiry"); + } + + void validate(const utility::string_t& token, const web::http::http_request& req, const scope& scope, const utility::string_t& audience, const web::uri& auth_server) const + { + using namespace jwt::experimental::details; + + // verify JWT is well formed + auto decoded_token = jwt::decode(utility::us2s(token)); + + // validate bearer token payload JSON + if (token_validation) + { + token_validation(web::json::value::parse(utility::s2us(decoded_token.get_payload()))); + } + + std::vector errors; + + if (validators.size()) + { + const auto validate_scope = !scope.name.empty(); + + for (const auto& validator : validators) + { + try + { + // verify the signature & some common claims, such as exp, iat, nbf etc + validator.verify(decoded_token); + + // common claims verified (i.e. validator/public key successfully verify the token's signature), + // from this point onwards any error detected will be treated as failure + + // verify Registered Claims + + // iss (Identifies principal that issued the JWT) + // The "iss" value is a case-sensitive string containing a StringOrURI value. + // see https://tools.ietf.org/html/rfc7519#section-4.1.1 + // iss is not needed to validate as this token may be coming from an alternative Authorization server, which would have a different iss then the current in used Authorization server. + + // sub (Identifies the subject of the JWT) + // hmm, not sure how to verify sub as it could be anything + // see https://tools.ietf.org/html/rfc7519#section-4.1.2 + + // aud (Identifies the recipients of the JWT) + // This claim MUST be a JSON array containing the fully resolved domain names of the intended recipients, or a domain name containing + // wild - card characters in order to target a subset of devices on a network. Such wild-carding of domain names is documented in RFC 4592. + // If aud claim does not match the fully resolved domain name of the resource server, the Resource Server MUST reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#aud + // see https://tools.ietf.org/html/rfc7519#section-4.1.3 + + auto verify_aud = [&decoded_token](const utility::string_t& audience_) + { + auto strip_trailing_dot = [](const std::string& audience_) { + auto audience = audience_; + if (!audience.empty() && U('.') == audience.back()) + { + audience.pop_back(); + } + return audience; + }; + + auto audience = strip_trailing_dot(utility::us2s(audience_)); + std::vector segments; + boost::split(segments, audience, boost::is_any_of(".")); + + const auto& auds = decoded_token.get_audience(); + for (const auto& aud_ : auds) + { + // strip the scheme (https://) if presented + auto aud = strip_trailing_dot(aud_); + web::http::uri aud_uri(utility::s2us(aud)); + if (!aud_uri.scheme().empty()) + { + aud = utility::us2s(aud_uri.host()); + } + + // is the audience an exact match to the token audience + if (audience == aud) + { + return true; + } + + // do reverse segment matching between audience and token audience + std::vector aud_segments; + boost::split(aud_segments, aud, boost::is_any_of(".")); + + if (segments.size() >= aud_segments.size() && aud_segments.size()) + { + // token audience got to be in wildcard domain name format, leftmost is a "*" charcater + // if not it is not going to match + // see https://tools.ietf.org/html/rfc4592#section-2.1.1 + if (aud_segments[0] != "*") + { + return false; + } + + // token audience is in wildcard domain name format + // let's do a segment to segment comparison between audience and token audience + bool matched{ true }; + auto idx = aud_segments.size() - 1; + for (auto it = aud_segments.rbegin(); it != aud_segments.rend() && matched; ++it) + { + if (idx && *it != segments[idx--]) + { + matched = false; + } + } + if (matched) + { + return true; + } + } + } + return false; + }; + if (!verify_aud(audience)) + { + throw insufficient_scope_exception(utility::us2s(audience) + " not found in audience"); + } + + // scope optional + // If scope claim does not contain the expected scope, the Resource Server reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope + auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) + { + if (decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + { + const auto& scope_claim = decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto scopes_set = scopes(utility::s2us(scope_claim.as_string())); + return (scopes_set.end() != std::find(scopes_set.begin(), scopes_set.end(), scope)); + } + return true; + }; + if (validate_scope && !verify_scope(scope)) + { + throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); + } + + // verify Private Claims + + // x-nmos-* (Contains information particular to the NMOS API the token is intended for) + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + auto verify_x_nmos_scope_claim = [&decoded_token, req](const std::string& x_nmos_scope_claim_, const std::string& path) + { + if (!decoded_token.has_payload_claim(x_nmos_scope_claim_)) { return false; } + const auto x_nmos_scope_claim = decoded_token.get_payload_claim(x_nmos_scope_claim_).to_json(); + + if (!x_nmos_scope_claim.is_null()) + { + auto accessible = [&x_nmos_scope_claim, req, &path](const std::string& access_right) + { + if (x_nmos_scope_claim.contains(access_right)) + { + auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); + for (auto& accessible_path : accessible_paths) + { + // construct path regex for regex comparison + + auto acc_path = accessible_path.get(); + // replace any '*' => '.*' + boost::replace_all(acc_path, "*", ".*"); + const bst::regex path_regex(acc_path); + if (bst::regex_match(path, path_regex)) + { + return true; + } + } + } + return false; + }; + + // write accessible + if ((web::http::methods::POST == req.method()) + || (web::http::methods::PUT == req.method()) + || (web::http::methods::PATCH == req.method()) + || (web::http::methods::DEL == req.method())) + { + return accessible("write"); + } + + // read accessible + if ((web::http::methods::OPTIONS == req.method()) + || (web::http::methods::GET == req.method()) + || (web::http::methods::HEAD == req.method())) + { + return accessible("read"); + } + } + return false; + }; + + // verify the relevant x-nmos-* private claim + if (validate_scope) + { + const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); + + // extract from /x-nmos/// + auto extract_path = [req](const nmos::experimental::scope& scope) + { + const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); + const auto request_uri = utility::us2s(req.request_uri().to_string()); + + if (bst::regex_search(request_uri, search_regex)) + { + auto path = bst::regex_replace(request_uri, search_regex, ""); + if (path.size() && ('/' == path[0])) + { + return path.erase(0, 1); + } + else + { + return std::string{}; + } + } + return std::string{};; + }; + const auto path = extract_path(scope); + + if (path.empty()) + { + // The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + + // if scope claim is presented, it has already verified eariler + if (!decoded_token.has_payload_claim(x_nmos_scope_claim) && !decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + { + // missing both x-nmos private claim and scope claim + throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); + } + } + else + { + // The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + + if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) + { + throw insufficient_scope_exception("claim x-nmos-" + utility::us2s(scope.name) + " " + utility::us2s(req.request_uri().to_string()) + " not accessible"); + } + } + } + + // token validate successfully + return; + } + catch (const insufficient_scope_exception&) + { + throw; + } + catch (const jwt::error::token_verification_exception& e) + { + throw std::invalid_argument(e.what()); + } + catch (const jwt::error::signature_verification_exception& e) + { + // ignore, try next validator + errors.push_back(e.what()); + } + } + } + else + { + // no public keys to validate token + errors.push_back("no public keys to validate access token"); + } + + // reaching here, there must be no matching public key for the token + + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + + const auto token_issuer = web::uri{ utility::s2us(decoded_token.get_issuer()) }; + + // if token is coming from an unknown issuer, do public keys fetch on token issuer, otherwise failed with no public keys! + if (!auth_server.has_same_authority(token_issuer)) + { + // verify token issuer + if (token_issuer.scheme() != web::uri_schemes::http && token_issuer.scheme() != web::uri_schemes::https) + { + errors.push_back("issuer must be 'http' or 'https'"); + throw std::runtime_error(format_errors(errors)); + } + + if (token_issuer.host().empty()) + { + errors.push_back("issuer must contain a hostname"); + throw std::runtime_error(format_errors(errors)); + } + + // no matching public keys for the token, re-fetch public keys from token issuer + throw no_matching_keys_exception(token_issuer, format_errors(errors)); + } + else + { + // no public keys to validate token + throw std::runtime_error("no public keys to validate access token"); + } + } + + // may throw + static utility::string_t client_id(const utility::string_t& token) + { + using namespace jwt::experimental::details; + + // verify JWT is well formed + auto decoded_token = jwt::decode(utility::us2s(token)); + // token does not guarantee to have client_id + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#client_id + if (decoded_token.has_payload_claim("client_id")) + { + const auto client_id = decoded_token.get_payload_claim("client_id"); + return utility::s2us(client_id.as_string()); + } + // azp is an OPTIONAL claim for OpenID Connect + // Authorized party - the party to which the ID Token was issued.If present, it MUST contain the OAuth 2.0 Client ID of this party. + // This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. + // It MAY be included even when the authorized party is the same as the sole audience. + // The azp value is a case sensitive string containing a StringOrURI value. + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken + else if (decoded_token.has_payload_claim("azp")) + { + const auto client_id = decoded_token.get_payload_claim("azp"); + return utility::s2us(client_id.as_string()); + } + return{}; + } + + // may throw + static web::uri token_issuer(const utility::string_t& token) + { + using namespace jwt::experimental::details; + + // verify JWT is well formed + auto decoded_token = jwt::decode(utility::us2s(token)); + return utility::s2us(decoded_token.get_issuer()); + } + + private: + std::string format_errors(const std::vector& errs) const + { + std::string separator; + std::stringstream ss; + for (const auto& err : errs) + { + ss << separator << err; + separator = ", "; + } + return ss.str(); + } + + private: + std::vector> validators; + token_validator token_validation; + }; + } + + jwt_validator::jwt_validator(const web::json::value& pubkeys, token_validator token_validation) + : impl(new details::jwt_validator_impl(pubkeys, token_validation)) + { + } + + bool jwt_validator::is_initialized() const + { + return impl ? true : false; + } + + void jwt_validator::validate_expiry(const utility::string_t& token) const + { + if (!impl) { throw std::runtime_error("JWT validator has not initiliased"); } + + impl->validate_expiry(token); + } + + void jwt_validator::validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server) const + { + if (!impl) { throw std::runtime_error("JWT validator has not initiliased"); } + + impl->validate(token, request, scope, audience, auth_server); + } + + utility::string_t jwt_validator::client_id(const utility::string_t& token) + { + return details::jwt_validator_impl::client_id(token); + } + + web::uri jwt_validator::token_issuer(const utility::string_t& token) + { + return details::jwt_validator_impl::token_issuer(token); + } + } +} diff --git a/Development/nmos/mdns.cpp b/Development/nmos/mdns.cpp index 73b17f1ca..4e49ffe8a 100644 --- a/Development/nmos/mdns.cpp +++ b/Development/nmos/mdns.cpp @@ -13,6 +13,7 @@ #include "mdns/service_advertiser.h" #include "mdns/service_discovery.h" #include "nmos/is09_versions.h" +#include "nmos/is10_versions.h" #include "nmos/random.h" namespace nmos @@ -106,13 +107,22 @@ namespace nmos } bool get_service_authorization(const nmos::service_type& service, const nmos::settings& settings) + { + // IS-09 System API does not use authorization + // See https://github.com/AMWA-TV/is-09/issues/21 + // IS-10 Authorization API does not use authorization + if (nmos::service_types::system == service || nmos::service_types::authorization == service) return false; + + return nmos::experimental::fields::client_authorization(settings) | nmos::experimental::fields::server_authorization(settings); + } + + bool is_api_authorization_protected(const nmos::service_type& service, const nmos::settings& settings) { // IS-09 System API does not use authorization // See https://github.com/AMWA-TV/is-09/issues/21 if (nmos::service_types::system == service) return false; - - const auto client_authorization = false; - return client_authorization; + + return nmos::experimental::fields::server_authorization(settings); } namespace details @@ -175,8 +185,27 @@ namespace nmos return mdns::parse_txt_record(records, txt_record_keys::pri, details::parse_pri_value, service_priorities::no_priority); } + namespace details + { + inline std::string make_api_selector_value(utility::string_t api_selector = {}) + { + return utility::us2s(api_selector); + } + + inline utility::string_t parse_api_selector_value(const std::string& api_selector) + { + return utility::s2us(api_selector); + } + } + + // find and parse the 'api_selector' TXT record (or return the default) + utility::string_t parse_api_selector_record(const mdns::structured_txt_records& records) + { + return mdns::parse_txt_record(records, txt_record_keys::api_selector, details::parse_api_selector_value, utility::string_t{}); + } + // make the required TXT records from the specified values (or sensible default values) - mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri, const std::set& api_ver, const service_protocol& api_proto, bool api_auth) + mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri, const std::set& api_ver, const service_protocol& api_proto, bool api_auth, const utility::string_t& selector) { if (service == nmos::service_types::node) { @@ -215,12 +244,12 @@ namespace nmos else if (service == nmos::service_types::authorization) { // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#dns-sd-txt-records - // hm, IS-10 Authorization may also need an 'api_selector' TXT record return { { txt_record_keys::api_proto, details::make_api_proto_value(api_proto) }, { txt_record_keys::api_ver, details::make_api_ver_value(api_ver) }, - { txt_record_keys::pri, details::make_pri_value(pri) } + { txt_record_keys::pri, details::make_pri_value(pri) }, + { txt_record_keys::api_selector, details::make_api_selector_value(selector) } }; } else if (service == nmos::service_types::mqtt) @@ -281,6 +310,7 @@ namespace nmos if (nmos::service_types::registration == service) return nmos::fields::registration_port(settings); if (nmos::service_types::register_ == service) return nmos::fields::registration_port(settings); if (nmos::service_types::system == service) return nmos::fields::system_port(settings); + if (nmos::service_types::authorization == service) return nmos::experimental::fields::authorization_port(settings); return 0; } @@ -291,6 +321,7 @@ namespace nmos if (nmos::service_types::registration == service) return "registration"; if (nmos::service_types::register_ == service) return "registration"; if (nmos::service_types::system == service) return "system"; + if (nmos::service_types::authorization == service) return "auth"; return{}; } @@ -303,6 +334,8 @@ namespace nmos { // the System API is defined by IS-09 (having been originally specified in JT-NM TR-1001-1:2018 Annex A) if (nmos::service_types::system == service) return nmos::is09_versions::from_settings(settings); + // the Authorization API is defined by IS-10 + if (nmos::service_types::authorization == service) return nmos::is10_versions::from_settings(settings); // all the other APIs are defined by IS-04, and should advertise consistent versions return nmos::is04_versions::from_settings(settings); } @@ -347,7 +380,7 @@ namespace nmos if (0 > instance_port_or_disabled) return; const auto instance_port = (uint16_t)instance_port_or_disabled; const auto api_ver = details::service_versions(service, settings); - const auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::get_service_authorization(service, settings)); + const auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::is_api_authorization_protected(service, settings)); const auto txt_records = mdns::make_txt_records(records); // advertise "_nmos-register._tcp" for v1.3 (and as an experimental extension, for lower versions) @@ -425,7 +458,7 @@ namespace nmos { const auto instance_name = service_name(service, settings); const auto api_ver = details::service_versions(service, settings); - auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::get_service_authorization(service, settings)); + auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::is_api_authorization_protected(service, settings)); records.insert(records.end(), std::make_move_iterator(add_records.begin()), std::make_move_iterator(add_records.end())); const auto txt_records = mdns::make_txt_records(records); @@ -471,8 +504,6 @@ namespace nmos namespace details { - typedef std::pair api_ver_pri; - typedef std::pair resolved_service; typedef std::vector resolved_services; std::vector get_resolved_hosts(const mdns::resolve_result& resolved, const nmos::service_protocol& resolved_proto, discovery_mode mode) @@ -515,7 +546,7 @@ namespace nmos // parse into structured TXT records auto records = mdns::parse_txt_records(resolved.txt_records); - // 'pri' must not be omitted for Registration API and Query API (see nmos::make_txt_records) + // 'pri' must not be omitted for Registration API, Query API and Authorization API (see nmos::make_txt_records) auto resolved_pri = nmos::parse_pri_record(records); if (service != nmos::service_types::node) { @@ -538,10 +569,24 @@ namespace nmos auto resolved_ver = std::find_first_of(resolved_vers.rbegin(), resolved_vers.rend(), api_ver.rbegin(), api_ver.rend()); if (resolved_vers.rend() == resolved_ver) return true; - auto resolved_uri = web::uri_builder() - .set_scheme(utility::s2us(resolved_proto)) - .set_port(resolved.port) - .set_path(U("/x-nmos/") + utility::s2us(details::service_api(service))); + // hmm, may be in the furture check for the matching 'api_selector' value + auto resolved_selector = nmos::parse_api_selector_record(records); + + auto resolved_uri = web::uri_builder(); + if (service == nmos::service_types::authorization) + { + resolved_uri + .set_scheme(utility::s2us(resolved_proto)) + .set_port(resolved.port) + .set_path(U("/.well-known/oauth-authorization-server")).append_path(!resolved_selector.empty() ? U("/") + resolved_selector : U("")); + } + else + { + resolved_uri + .set_scheme(utility::s2us(resolved_proto)) + .set_port(resolved.port) + .set_path(U("/x-nmos/") + utility::s2us(details::service_api(service))); + } auto resolved_hosts = get_resolved_hosts(resolved, resolved_proto, discovery_mode); @@ -568,11 +613,12 @@ namespace nmos std::pair service_priorities(const nmos::service_type& service, const nmos::settings& settings) { + if (nmos::service_types::authorization == service) return { nmos::fields::authorization_highest_pri(settings), nmos::fields::authorization_lowest_pri(settings) }; return { nmos::fields::highest_pri(settings), nmos::fields::lowest_pri(settings) }; } } - pplx::task> resolve_service(mdns::service_discovery& discovery, discovery_mode mode, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) + pplx::task> resolve_service_(mdns::service_discovery& discovery, discovery_mode mode, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) { const auto absolute_timeout = std::chrono::steady_clock::now() + timeout; @@ -641,11 +687,11 @@ namespace nmos { // since each advertisement may be discovered via multiple interfaces and, in the case of the Registration API, via two service types // remove duplicate uris, after sorting to ensure the highest advertised priority is kept for each - std::stable_sort(results->begin(), results->end(), [](const details::resolved_service& lhs, const details::resolved_service& rhs) + std::stable_sort(results->begin(), results->end(), [](const resolved_service& lhs, const resolved_service& rhs) { return lhs.second < rhs.second || (lhs.second == rhs.second && details::less_api_ver_pri(lhs.first, rhs.first)); }); - results->erase(std::unique(results->begin(), results->end(), [](const details::resolved_service& lhs, const details::resolved_service& rhs) + results->erase(std::unique(results->begin(), results->end(), [](const resolved_service& lhs, const resolved_service& rhs) { return lhs.second == rhs.second; }), results->end()); @@ -660,14 +706,28 @@ namespace nmos } // "Given multiple returned Registration APIs, the Node orders these based on their advertised priority (TXT pri)" - std::stable_sort(results->begin(), results->end(), [](const details::resolved_service& lhs, const details::resolved_service& rhs) + std::stable_sort(results->begin(), results->end(), [](const resolved_service& lhs, const resolved_service& rhs) { // hmm, for the moment, the scheme is *not* considered; one might want to prefer 'https' over 'http'? return details::less_api_ver_pri(lhs.first, rhs.first); }); + // return the randomized services + std::list resolved_services; + for (const auto& result : *results) + { + resolved_services.push_back(result); + } + return resolved_services; + }); + } + + pplx::task> resolve_service(mdns::service_discovery& discovery, discovery_mode mode, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) + { + return resolve_service_(discovery, mode, service, browse_domain, api_ver, priorities, api_proto, api_auth, randomize, timeout, token).then([](std::list resolved_services) + { // add the version to each uri - return boost::copy_range>(*results | boost::adaptors::transformed([](const details::resolved_service& s) + return boost::copy_range>(resolved_services | boost::adaptors::transformed([](const resolved_service& s) { return web::uri_builder(s.second).append_path(U("/") + make_api_version(s.first.first)).to_uri(); })); @@ -695,8 +755,33 @@ namespace nmos // use a short timeout that's long enough to ensure the daemon's cache is exhausted // when no cancellation token is specified const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1; - + return resolve_service(discovery, mode, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::seconds(timeout), token); } + + // helper function for resolving instances of the specified service (API) + // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) + { + return resolve_service_(discovery, discovery_mode_default, service, browse_domain, api_ver, priorities, api_proto, api_auth, randomize, timeout, token); + } + + // helper function for resolving instances of the specified service (API) based on the specified settings + // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token) + { + const auto mode = discovery_mode(nmos::experimental::fields::discovery_mode(settings)); + const auto browse_domain = utility::us2s(nmos::get_domain(settings)); + const auto versions = details::service_versions(service, settings); + const auto priorities = details::service_priorities(service, settings); + const auto protocols = std::set{ nmos::get_service_protocol(service, settings) }; + const auto authorization = std::set{ nmos::get_service_authorization(service, settings) }; + + // use a short timeout that's long enough to ensure the daemon's cache is exhausted + // when no cancellation token is specified + const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1; + + return resolve_service_(discovery, mode, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::seconds(timeout), token); + } } } diff --git a/Development/nmos/mdns.h b/Development/nmos/mdns.h index 9a257a1a3..8267ab850 100644 --- a/Development/nmos/mdns.h +++ b/Development/nmos/mdns.h @@ -61,7 +61,7 @@ namespace nmos // and https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#dns-sd-txt-records const service_protocol http{ "http" }; const service_protocol https{ "https" }; - + // Values for the 'api_proto' TXT record for MQTT broker advertisements // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#7-broker-discovery const service_protocol mqtt{ "mqtt" }; @@ -122,7 +122,7 @@ namespace nmos service_priority parse_pri_record(const mdns::structured_txt_records& records); // make the required TXT records from the specified values (or sensible default values) - mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri = service_priorities::highest_development_priority, const std::set& api_ver = is04_versions::all, const service_protocol& api_proto = service_protocols::http, bool api_auth = false); + mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri = service_priorities::highest_development_priority, const std::set& api_ver = is04_versions::all, const service_protocol& api_proto = service_protocols::http, bool api_auth = false, const utility::string_t& selector = {}); // "The value of each of the ['ver_' TXT records] should be an unsigned 8-bit integer initialised // to '0'. This integer MUST be incremented and mDNS TXT record updated whenever a change is made @@ -172,6 +172,25 @@ namespace nmos // helper function for resolving instances of the specified service (API) based on the specified settings // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + typedef std::pair api_ver_pri; + typedef std::pair resolved_service; + + // helper function for resolving instances of the specified service (API) + // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + // helper function for resolving instances of the specified service (API) based on the specified options or defaults + // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly + template + inline pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain = {}, const std::set& api_ver = nmos::is04_versions::all, const std::pair& priorities = { service_priorities::highest_active_priority, service_priorities::no_priority }, const std::set& api_proto = nmos::service_protocols::all, const std::set& api_auth = { false, true }, bool randomize = true, const std::chrono::duration& timeout = std::chrono::seconds(mdns::default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + return resolve_service_(discovery, service, browse_domain, api_ver, api_proto, api_auth, randomize, std::chrono::duration_cast(timeout), token); + } + + // helper function for resolving instances of the specified service (API) based on the specified settings + // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token = pplx::cancellation_token::none()); } } diff --git a/Development/nmos/node_api.cpp b/Development/nmos/node_api.cpp index 852cdca13..a40708bec 100644 --- a/Development/nmos/node_api.cpp +++ b/Development/nmos/node_api.cpp @@ -7,14 +7,14 @@ #include "nmos/is04_versions.h" #include "nmos/json_schema.h" #include "nmos/model.h" +#include "nmos/scope.h" #include "nmos/slog.h" -#include "cpprest/host_utils.h" namespace nmos { web::http::experimental::listener::api_router make_unmounted_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate); - web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate) + web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -32,6 +32,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + node_api.support(U("/x-nmos/") + nmos::patterns::node_api.pattern + U("/?"), validate_authorization); + node_api.support(U("/x-nmos/") + nmos::patterns::node_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is04_versions::from_settings(model.settings); }); node_api.support(U("/x-nmos/") + nmos::patterns::node_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { diff --git a/Development/nmos/node_api.h b/Development/nmos/node_api.h index 9c5ee8526..2d457172c 100644 --- a/Development/nmos/node_api.h +++ b/Development/nmos/node_api.h @@ -10,7 +10,12 @@ namespace nmos { struct model; - web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate); + web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate) + { + return make_node_api(model, std::move(target_handler), {}, gate); + } } #endif diff --git a/Development/nmos/node_api_target_handler.cpp b/Development/nmos/node_api_target_handler.cpp index 725df294c..30cd64e88 100644 --- a/Development/nmos/node_api_target_handler.cpp +++ b/Development/nmos/node_api_target_handler.cpp @@ -8,15 +8,16 @@ #include "nmos/json_fields.h" #include "nmos/media_type.h" // for nmos::media_types::application_sdp #include "nmos/model.h" +#include "nmos/scope.h" #include "nmos/slog.h" namespace nmos { // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser and the specified validator // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) - node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) + node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::authorization_config_handler make_authorization_config) { - return [&model, load_ca_certificates, parse_transport_file, validate_merged](const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate) + return [&model, load_ca_certificates, parse_transport_file, validate_merged, make_authorization_config](const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate) { using web::json::value; using web::json::value_of; @@ -29,7 +30,7 @@ namespace nmos // if manifest_href is null, this will throw json_exception which will be reported appropriately as 400 Bad Request const auto manifest_href = nmos::fields::manifest_href(sender_data).as_string(); - web::http::client::http_client client(manifest_href, nmos::with_read_lock(model.mutex, [&, load_ca_certificates] { return nmos::make_http_client_config(model.settings, load_ca_certificates, gate); })); + web::http::client::http_client client(manifest_href, nmos::with_read_lock(model.mutex, [&, load_ca_certificates, make_authorization_config] { return nmos::make_http_client_config(model.settings, load_ca_certificates, make_authorization_config, gate); })); return api_request(client, web::http::methods::GET, gate).then([manifest_href, &gate](web::http::http_response res) { if (res.status_code() != web::http::status_codes::OK) diff --git a/Development/nmos/node_api_target_handler.h b/Development/nmos/node_api_target_handler.h index 076cb7574..ac368ec06 100644 --- a/Development/nmos/node_api_target_handler.h +++ b/Development/nmos/node_api_target_handler.h @@ -1,6 +1,7 @@ #ifndef NMOS_NODE_API_TARGET_HANDLER_H #define NMOS_NODE_API_TARGET_HANDLER_H +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" #include "nmos/connection_api.h" @@ -21,7 +22,12 @@ namespace nmos // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified handlers // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) - node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged); + node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::authorization_config_handler make_authorization_config); + + inline node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) + { + return make_node_api_target_handler(model, std::move(load_ca_certificates), std::move(parse_transport_file), std::move(validate_merged), {}); + } inline node_api_target_handler make_node_api_target_handler(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) { diff --git a/Development/nmos/node_behaviour.cpp b/Development/nmos/node_behaviour.cpp index 2b009e301..355860a6f 100644 --- a/Development/nmos/node_behaviour.cpp +++ b/Development/nmos/node_behaviour.cpp @@ -7,6 +7,7 @@ #include "mdns/service_discovery.h" #include "nmos/api_downgrade.h" #include "nmos/api_utils.h" // for nmos::type_from_resourceType +#include "nmos/authorization_state.h" #include "nmos/client_utils.h" #include "nmos/mdns.h" #include "nmos/model.h" @@ -21,11 +22,11 @@ namespace nmos { namespace details { - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); // registered operation - void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); - void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate); + void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate); + void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); // peer to peer operation void peer_to_peer_operation(nmos::model& model, const nmos::id& grain_id, mdns::service_discovery& discovery, mdns::service_advertiser& advertiser, slog::base_gate& gate); @@ -42,6 +43,17 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); + + mdns::service_advertiser advertiser(gate); + mdns::service_advertiser_guard advertiser_guard(advertiser); + + mdns::service_discovery discovery(gate); + + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(make_authorization_config), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); @@ -51,31 +63,43 @@ namespace nmos mdns::service_discovery discovery(gate); - details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, {}, advertiser, discovery, gate); } // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate_) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); - details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(make_authorization_config), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + } + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + { + node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, {}, advertiser, discovery, gate); } // uses the default DNS-SD implementation + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate) + { + node_behaviour_thread(model, load_ca_certificates, {}, std::move(make_authorization_config), std::move(get_authorization_bearer_token), gate); + } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { node_behaviour_thread(model, load_ca_certificates, {}, gate); } // uses the specified DNS-SD implementation + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + { + node_behaviour_thread(model, load_ca_certificates, {}, std::move(make_authorization_config), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { node_behaviour_thread(model, load_ca_certificates, {}, advertiser, discovery, gate); } - void details::node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + void details::node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { // The possible states of node behaviour represent the two primary modes (registered operation and peer-to-peer operation) // and a few hopefully ephemeral states as the node works through the "Standard Registration Sequences". @@ -160,7 +184,7 @@ namespace nmos case initial_registration: // "5. The Node registers itself with the Registration API by taking the object it holds under the Node API's /self resource and POSTing this to the Registration API." - details::initial_registration(self_id, model, grain_id, load_ca_certificates, gate); + details::initial_registration(self_id, model, grain_id, load_ca_certificates, make_authorization_config, gate); if (details::has_discovered_registration_services(model)) { @@ -177,7 +201,7 @@ namespace nmos case registered_operation: // "6. The Node persists itself in the registry by issuing heartbeats." // "7. The Node registers its other resources (from /devices, /sources etc) with the Registration API." - details::registered_operation(self_id, model, grain_id, load_ca_certificates, registration_changed, gate); + details::registered_operation(self_id, model, grain_id, load_ca_certificates, registration_changed, make_authorization_config, get_authorization_bearer_token, gate); if (details::has_discovered_registration_services(model)) { @@ -261,7 +285,7 @@ namespace nmos }).get(); with_write_lock(model.mutex, [&] - { + { if (!registration_services.empty()) { slog::log(gate, SLOG_FLF) << "Discovered " << registration_services.size() << " Registration API(s)"; @@ -470,16 +494,16 @@ namespace nmos handle_registration_error_conditions(response, false, gate, operation); } - web::http::client::http_client_config make_registration_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + web::http::client::http_client_config make_registration_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), gate); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), std::move(make_authorization_config), gate); config.set_timeout(std::chrono::seconds(nmos::fields::registration_request_max(settings))); return config; } - web::http::client::http_client_config make_heartbeat_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + web::http::client::http_client_config make_heartbeat_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), gate); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), std::move(make_authorization_config), gate); config.set_timeout(std::chrono::seconds(nmos::fields::registration_heartbeat_max(settings))); return config; } @@ -657,7 +681,7 @@ namespace nmos } // there is significant similarity between initial_registration and registered_operation but I'm too tired to refactor again right now... - void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting initial registration"; @@ -744,7 +768,7 @@ namespace nmos grain.updated = strictly_increasing_update(resources); }); - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, gate))); + registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); } events = web::json::value::array(); @@ -815,7 +839,7 @@ namespace nmos request.wait(); } - void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate) + void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Adopting registered operation"; @@ -838,6 +862,9 @@ namespace nmos std::chrono::steady_clock::time_point heartbeat_time; + web::http::oauth2::experimental::oauth2_token registration_bearer_token; + web::http::oauth2::experimental::oauth2_token heartbeat_bearer_token; + // background tasks may read/write the above local state by reference pplx::cancellation_token_source cancellation_source; pplx::task request = pplx::task_from_result(); @@ -881,8 +908,8 @@ namespace nmos const auto registry_version = parse_api_version(web::uri::split_path(base_uri.path()).back()); if (registry_version != grain->version) break; - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, gate))); - heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, gate))); + registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); // "The first interaction with a new Registration API [after a server side or connectivity issue] // should be a heartbeat to confirm whether whether the Node is still present in the registry" @@ -916,15 +943,28 @@ namespace nmos } model.notify(); - }).then([=, &heartbeat_time, &heartbeat_client, &gate] + }).then([=, &model, &heartbeat_time, &heartbeat_client, &heartbeat_bearer_token, &gate] { // "6. The Node persists itself in the registry by issuing heartbeats." - return pplx::do_while([=, &heartbeat_time, &heartbeat_client, &gate] + return pplx::do_while([=, &model, &heartbeat_time, &heartbeat_client, &heartbeat_bearer_token, &gate] { - return pplx::complete_at(heartbeat_time + heartbeat_interval, token).then([=, &heartbeat_time, &heartbeat_client, &gate]() mutable + return pplx::complete_at(heartbeat_time + heartbeat_interval, token).then([=, &model, &heartbeat_time, &heartbeat_client, &heartbeat_bearer_token, &gate]() mutable { heartbeat_time = std::chrono::steady_clock::now(); + + if (get_authorization_bearer_token) + { + const auto bearer_token = get_authorization_bearer_token(); + if (heartbeat_bearer_token.access_token() != bearer_token.access_token()) + { + slog::log(gate, SLOG_FLF) << "Update heartbeat client with new authorization token"; + + heartbeat_bearer_token = bearer_token; + heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + } + } + return update_node_health(*heartbeat_client, self_id, gate, token); }); }, token); @@ -969,6 +1009,20 @@ namespace nmos const auto event_type = get_resource_event_type(events.at(0)); auto token = cancellation_source.get_token(); + + // renew regsitration_client if bearer token has changed + if (get_authorization_bearer_token) + { + const auto bearer_token = get_authorization_bearer_token(); + if (registration_bearer_token.access_token() != bearer_token.access_token()) + { + slog::log(gate, SLOG_FLF) << "Update registration client with new authorization token"; + + registration_bearer_token = bearer_token; + registration_client.reset(new web::http::client::http_client(registration_client->base_uri(), make_registration_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + } + } + request = details::request_registration(*registration_client, events.at(0), gate, token).then([&](pplx::task finally) { auto lock = model.write_lock(); // in order to update local state diff --git a/Development/nmos/node_behaviour.h b/Development/nmos/node_behaviour.h index f7b3f2480..53293eb2d 100644 --- a/Development/nmos/node_behaviour.h +++ b/Development/nmos/node_behaviour.h @@ -2,6 +2,7 @@ #define NMOS_NODE_BEHAVIOUR_H #include +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" namespace web @@ -35,16 +36,20 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate); // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); // uses the default DNS-SD implementation + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // uses the specified DNS-SD implementation + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); } diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index 7b65b25b2..8e5af4744 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -35,7 +35,8 @@ namespace nmos web::json::push_back(data[U("api")][U("endpoints")], value_of({ { U("host"), host }, { U("port"), uri.port() }, - { U("protocol"), uri.scheme() } + { U("protocol"), uri.scheme() }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } diff --git a/Development/nmos/node_resources.cpp b/Development/nmos/node_resources.cpp index 7c75cf01a..e177c833d 100644 --- a/Development/nmos/node_resources.cpp +++ b/Development/nmos/node_resources.cpp @@ -55,7 +55,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), connection_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } @@ -75,7 +76,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), events_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } @@ -100,7 +102,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), channelmapping_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } @@ -120,7 +123,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), manifest_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } diff --git a/Development/nmos/node_resources.h b/Development/nmos/node_resources.h index 2598867bb..a1b9f6555 100644 --- a/Development/nmos/node_resources.h +++ b/Development/nmos/node_resources.h @@ -113,7 +113,7 @@ namespace nmos { web::uri make_manifest_api_manifest(const nmos::id& sender_id, const nmos::settings& settings); } - + nmos::resource make_sender(const nmos::id& id, const nmos::id& flow_id, const nmos::id& device_id, const std::vector& interfaces, const nmos::settings& settings); // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_core.html diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 2e4469788..be313090d 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -19,7 +19,7 @@ namespace nmos { namespace experimental { - // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API + // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API, the IS-10 Authorization API // and the experimental Logging API and Settings API, according to the specified data models and callbacks nmos::server make_node_server(nmos::node_model& node_model, nmos::experimental::node_implementation node_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate) { @@ -47,22 +47,25 @@ namespace nmos // Configure the Node API - nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged); - node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, gate)); + nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged, node_implementation.make_authorization_config); + auto validate_authorization = node_implementation.validate_authorization; + node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, validate_authorization ? validate_authorization(nmos::experimental::scopes::node) : nullptr, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); // Configure the Connection API - node_server.api_routers[{ {}, nmos::fields::connection_port(node_model.settings) }].mount({}, nmos::make_connection_api(node_model, node_implementation.parse_transport_file, node_implementation.validate_staged, gate)); + node_server.api_routers[{ {}, nmos::fields::connection_port(node_model.settings) }].mount({}, nmos::make_connection_api(node_model, node_implementation.parse_transport_file, node_implementation.validate_staged, validate_authorization ? validate_authorization(nmos::experimental::scopes::connection) : nullptr, gate)); // Configure the Events API - node_server.api_routers[{ {}, nmos::fields::events_port(node_model.settings) }].mount({}, nmos::make_events_api(node_model, gate)); + + node_server.api_routers[{ {}, nmos::fields::events_port(node_model.settings) }].mount({}, nmos::make_events_api(node_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::events) : nullptr, gate)); // Configure the Channel Mapping API - node_server.api_routers[{ {}, nmos::fields::channelmapping_port(node_model.settings) }].mount({}, nmos::make_channelmapping_api(node_model, node_implementation.validate_map, gate)); + + node_server.api_routers[{ {}, nmos::fields::channelmapping_port(node_model.settings) }].mount({}, nmos::make_channelmapping_api(node_model, node_implementation.validate_map, validate_authorization ? validate_authorization(nmos::experimental::scopes::channelmapping) : nullptr, gate)); auto& events_ws_api = node_server.ws_handlers[{ {}, nmos::fields::events_ws_port(node_model.settings) }]; - events_ws_api.first = nmos::make_events_ws_api(node_model, events_ws_api.second, gate); + events_ws_api.first = nmos::make_events_ws_api(node_model, events_ws_api.second, node_implementation.ws_validate_authorization, gate); // Set up the listeners for each HTTP API port @@ -101,8 +104,10 @@ namespace nmos auto set_transportfile = node_implementation.set_transportfile; auto connection_activated = node_implementation.connection_activated; auto channelmapping_activated = node_implementation.channelmapping_activated; + auto make_authorization_config = node_implementation.make_authorization_config; + auto get_authorization_bearer_token = node_implementation.get_authorization_bearer_token; node_server.thread_functions.assign({ - [&, load_ca_certificates, registration_changed] { nmos::node_behaviour_thread(node_model, load_ca_certificates, registration_changed, gate); }, + [&, load_ca_certificates, registration_changed, make_authorization_config, get_authorization_bearer_token] { nmos::node_behaviour_thread(node_model, load_ca_certificates, registration_changed, make_authorization_config, get_authorization_bearer_token, gate); }, [&] { nmos::send_events_ws_messages_thread(events_ws_listener, node_model, events_ws_api.second, gate); }, [&] { nmos::erase_expired_events_resources_thread(node_model, gate); }, [&, resolve_auto, set_transportfile, connection_activated] { nmos::connection_activation_thread(node_model, resolve_auto, set_transportfile, connection_activated, gate); }, diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index dc8f4efa8..ea8474a8f 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -1,6 +1,7 @@ #ifndef NMOS_NODE_SERVER_H #define NMOS_NODE_SERVER_H +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" #include "nmos/channelmapping_api.h" #include "nmos/channelmapping_activation.h" @@ -9,6 +10,7 @@ #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" #include "nmos/ocsp_response_handler.h" +#include "nmos/ws_api_utils.h" namespace nmos { @@ -24,7 +26,7 @@ namespace nmos // underlying implementation into the server instance for the NMOS Node struct node_implementation { - node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response) + node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, authorization_config_handler make_authorization_config, authorization_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code) : load_server_certificates(std::move(load_server_certificates)) , load_dh_param(std::move(load_dh_param)) , load_ca_certificates(std::move(load_ca_certificates)) @@ -36,6 +38,14 @@ namespace nmos , set_transportfile(std::move(set_transportfile)) , connection_activated(std::move(connection_activated)) , get_ocsp_response(std::move(get_ocsp_response)) + , make_authorization_config(std::move(make_authorization_config)) + , get_authorization_bearer_token(std::move(get_authorization_bearer_token)) + , validate_authorization(std::move(validate_authorization)) + , ws_validate_authorization(std::move(ws_validate_authorization)) + , load_rsa_private_keys(std::move(load_rsa_private_keys)) + , load_authorization_clients(std::move(load_authorization_clients)) + , save_authorization_client(std::move(save_authorization_client)) + , request_authorization_code(std::move(request_authorization_code)) {} // use the default constructor and chaining member functions for fluent initialization @@ -57,6 +67,14 @@ namespace nmos node_implementation& on_validate_channelmapping_output_map(nmos::details::channelmapping_output_map_validator validate_map) { this->validate_map = std::move(validate_map); return *this; } node_implementation& on_channelmapping_activated(nmos::channelmapping_activation_handler channelmapping_activated) { this->channelmapping_activated = std::move(channelmapping_activated); return *this; } node_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } + node_implementation& on_make_authorization_config(authorization_config_handler make_authorization_config) { this->make_authorization_config = std::move(make_authorization_config); return *this; } + node_implementation& on_get_authorization_bearer_token(authorization_token_handler get_authorization_bearer_token) { this->get_authorization_bearer_token = std::move(get_authorization_bearer_token); return *this; } + node_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return *this; } + node_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } + node_implementation& on_load_rsa_private_keys(nmos::load_rsa_private_keys_handler load_rsa_private_keys) { this->load_rsa_private_keys = std::move(load_rsa_private_keys); return *this; } + node_implementation& on_load_authorization_clients(load_authorization_clients_handler load_authorization_clients) { this->load_authorization_clients = std::move(load_authorization_clients); return *this; } + node_implementation& on_save_authorization_client(save_authorization_client_handler save_authorization_client) { this->save_authorization_client = std::move(save_authorization_client); return *this; } + node_implementation& on_request_authorization_code(request_authorization_code_handler request_authorization_code) { this->request_authorization_code = std::move(request_authorization_code); return *this; } // deprecated, use on_validate_connection_resource_patch node_implementation& on_validate_merged(nmos::details::connection_resource_patch_validator validate_merged) { return on_validate_connection_resource_patch(std::move(validate_merged)); } @@ -86,6 +104,15 @@ namespace nmos nmos::channelmapping_activation_handler channelmapping_activated; nmos::ocsp_response_handler get_ocsp_response; + + authorization_config_handler make_authorization_config; + authorization_token_handler get_authorization_bearer_token; + validate_authorization_handler validate_authorization; + ws_validate_authorization_handler ws_validate_authorization; + nmos::load_rsa_private_keys_handler load_rsa_private_keys; + load_authorization_clients_handler load_authorization_clients; + save_authorization_client_handler save_authorization_client; + request_authorization_code_handler request_authorization_code; }; // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API diff --git a/Development/nmos/query_api.cpp b/Development/nmos/query_api.cpp index 0e4da6cb4..a9d79e0a5 100644 --- a/Development/nmos/query_api.cpp +++ b/Development/nmos/query_api.cpp @@ -17,7 +17,7 @@ namespace nmos { inline web::http::experimental::listener::api_router make_unmounted_query_api(nmos::registry_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -35,6 +35,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + query_api.support(U("/x-nmos/") + nmos::patterns::query_api.pattern + U("/?"), validate_authorization); + query_api.support(U("/x-nmos/") + nmos::patterns::query_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is04_versions::from_settings(model.settings); }); query_api.support(U("/x-nmos/") + nmos::patterns::query_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { diff --git a/Development/nmos/query_api.h b/Development/nmos/query_api.h index 2c372d7b1..ec1200368 100644 --- a/Development/nmos/query_api.h +++ b/Development/nmos/query_api.h @@ -15,7 +15,12 @@ namespace nmos { struct registry_model; - web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, slog::base_gate& gate) + { + return make_query_api(model, {}, gate); + } struct resource_paging; diff --git a/Development/nmos/query_ws_api.cpp b/Development/nmos/query_ws_api.cpp index 2497f39ad..32e3a62fe 100644 --- a/Development/nmos/query_ws_api.cpp +++ b/Development/nmos/query_ws_api.cpp @@ -5,17 +5,18 @@ #include "nmos/query_utils.h" #include "nmos/rational.h" #include "nmos/thread_utils.h" // for wait_until +#include "nmos/scope.h" #include "nmos/slog.h" #include "nmos/version.h" namespace nmos { - web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, slog::base_gate& gate_) + web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate_) { - return [&model, &gate_](web::http::http_request req) + return [&model, ws_validate_authorization, &gate_](web::http::http_request req) { nmos::ws_api_gate gate(gate_, req.request_uri()); - auto lock = model.read_lock(); + auto lock = model.write_lock(); auto& resources = model.registry_resources; // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket @@ -23,6 +24,11 @@ namespace nmos // Clients MAY use a "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 + if (ws_validate_authorization) + { + if (!ws_validate_authorization(req, nmos::experimental::scopes::query)) { return false; } + } + // For now, to determine whether the "resource name" is valid, only look at the path, and ignore any query parameters const auto& ws_resource_path = req.request_uri().path(); slog::log(gate, SLOG_FLF) << "Validating websocket connection to: " << ws_resource_path; diff --git a/Development/nmos/query_ws_api.h b/Development/nmos/query_ws_api.h index b5034ff8f..bafc96512 100644 --- a/Development/nmos/query_ws_api.h +++ b/Development/nmos/query_ws_api.h @@ -1,7 +1,9 @@ #ifndef NMOS_QUERY_WS_API_H #define NMOS_QUERY_WS_API_H +#include "nmos/authorization_handlers.h" #include "nmos/websockets.h" +#include "nmos/ws_api_utils.h" namespace slog { @@ -14,17 +16,17 @@ namespace nmos { struct registry_model; - web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, slog::base_gate& gate); + web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate); // note, model mutex is assumed to also protect websockets web::websockets::experimental::listener::open_handler make_query_ws_open_handler(const nmos::id& source_id, nmos::registry_model& model, nmos::websockets& websockets, slog::base_gate& gate); // note, model mutex is assumed to also protect websockets web::websockets::experimental::listener::close_handler make_query_ws_close_handler(nmos::registry_model& model, nmos::websockets& websockets, slog::base_gate& gate); // note, model mutex is assumed to also protect websockets - inline web::websockets::experimental::listener::websocket_listener_handlers make_query_ws_api(const nmos::id& source_id, nmos::registry_model& model, nmos::websockets& websockets, slog::base_gate& gate) + inline web::websockets::experimental::listener::websocket_listener_handlers make_query_ws_api(const nmos::id& source_id, nmos::registry_model& model, nmos::websockets& websockets, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate) { return{ - nmos::make_query_ws_validate_handler(model, gate), + nmos::make_query_ws_validate_handler(model, ws_validate_authorization, gate), nmos::make_query_ws_open_handler(source_id, model, websockets, gate), nmos::make_query_ws_close_handler(model, websockets, gate), {} diff --git a/Development/nmos/registration_api.cpp b/Development/nmos/registration_api.cpp index 9d174eb75..2222e57b8 100644 --- a/Development/nmos/registration_api.cpp +++ b/Development/nmos/registration_api.cpp @@ -2,8 +2,10 @@ #include #include "cpprest/json_validator.h" +#include "cpprest/resource_server_error.h" #include "nmos/api_downgrade.h" // for details::make_permitted_downgrade_error #include "nmos/api_utils.h" +#include "nmos/authorization.h" #include "nmos/is04_versions.h" #include "nmos/json_schema.h" #include "nmos/log_manip.h" @@ -68,7 +70,7 @@ namespace nmos inline web::http::experimental::listener::api_router make_unmounted_registration_api(nmos::registry_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -86,6 +88,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + registration_api.support(U("/x-nmos/") + nmos::patterns::registration_api.pattern + U("/?"), validate_authorization); + registration_api.support(U("/x-nmos/") + nmos::patterns::registration_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is04_versions::from_settings(model.settings); }); registration_api.support(U("/x-nmos/") + nmos::patterns::registration_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -159,6 +167,11 @@ namespace nmos { return nmos::make_api_version(request_version) + U(" request conflicts with the existing ") + nmos::make_api_version(super_resource_version) + U(" registration of the parent"); } + + inline utility::string_t make_valid_client_id_error(const utility::string_t& request_client_id) + { + return U("request for resource modification with invalid client_id ") + request_client_id; + } } namespace details @@ -396,15 +409,39 @@ namespace nmos // always reject updates that would modify resource type or super-resource if (valid_type && valid_super_id_type && (valid || allow_invalid_resources)) { + // Registry MUST register the Client ID of the client performing the registration. Subsequent requests to modify or delete a registered + // resource MUST validate the Client ID to ensure that clients do not, maliciously or incorrectly, alter resources belonging to other nodes + // see https://specs.amwa.tv/bcp-003-02/releases/v1.0.0/docs/1.0._Authorization_Practice.html#registry-client-authorization + utility::string_t client_id; + if (nmos::experimental::fields::server_authorization(model.settings)) + { + // get client_id from header's access token + client_id = nmos::experimental::get_client_id(req.headers(), gate); + } + if (creating) { - nmos::resource created_resource{ version, type, data, false }; + nmos::resource created_resource{ version, type, data, false, client_id }; set_reply(res, status_codes::Created, data); res.headers().add(web::http::header_names::location, make_registration_api_resource_location(created_resource)); resource = insert_resource(resources, std::move(created_resource), allow_invalid_resources).first; } + // invalid Client ID, reject resource modification + // see https://specs.amwa.tv/bcp-003-02/releases/v1.0.0/docs/1.0._Authorization_Practice.html#registry-client-authorization + else if (client_id != resource->client_id) + { + auto req_host = web::http::get_host_port(req).first; + if (req_host.empty()) + { + req_host = nmos::get_host(model.settings); + } + const auto error_description = details::make_valid_client_id_error(client_id); + const utility::string_t auth_params{ U("Bearer realm=") + req_host + U(",error=") + web::http::oauth2::experimental::resource_server_errors::insufficient_scope.name + U(",error_description=") + error_description }; + res.headers().add(web::http::header_names::www_authenticate, auth_params); + set_error_reply(res, status_codes::Forbidden, error_description); + } else { set_reply(res, status_codes::OK, data); @@ -416,13 +453,17 @@ namespace nmos }); } - // experimental extension, for debugging - res.headers().add(U("X-Paging-Timestamp"), make_version(resource->updated)); + // resource created/updated + if (client_id == resource->client_id) + { + // experimental extension, for debugging + res.headers().add(U("X-Paging-Timestamp"), make_version(resource->updated)); - slog::log(gate, SLOG_FLF) << "At " << nmos::make_version(nmos::tai_now()) << ", the registry contains " << nmos::put_resources_statistics(resources); + slog::log(gate, SLOG_FLF) << "At " << nmos::make_version(nmos::tai_now()) << ", the registry contains " << nmos::put_resources_statistics(resources); - slog::log(gate, SLOG_FLF) << "Notifying query websockets thread"; // and anyone else who cares... - model.notify(); + slog::log(gate, SLOG_FLF) << "Notifying query websockets thread"; // and anyone else who cares... + model.notify(); + } } else if (!valid_api_version) { diff --git a/Development/nmos/registration_api.h b/Development/nmos/registration_api.h index d69fe4844..f254e02cd 100644 --- a/Development/nmos/registration_api.h +++ b/Development/nmos/registration_api.h @@ -16,7 +16,12 @@ namespace nmos void erase_expired_resources_thread(nmos::registry_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, slog::base_gate& gate) + { + return make_registration_api(model, {}, gate); + } } #endif diff --git a/Development/nmos/registry_server.cpp b/Development/nmos/registry_server.cpp index 6fc2f7314..2732b5115 100644 --- a/Development/nmos/registry_server.cpp +++ b/Development/nmos/registry_server.cpp @@ -27,7 +27,7 @@ namespace nmos namespace experimental { - // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API + // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API, the IS-10 Authorization API // and the experimental DNS-SD Browsing API, Logging API and Settings API, according to the specified data models nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::registry_implementation registry_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate) { @@ -62,22 +62,23 @@ namespace nmos // Configure the Query API - registry_server.api_routers[{ {}, nmos::fields::query_port(registry_model.settings) }].mount({}, nmos::make_query_api(registry_model, gate)); + auto validate_authorization = registry_implementation.validate_authorization; + registry_server.api_routers[{ {}, nmos::fields::query_port(registry_model.settings) }].mount({}, nmos::make_query_api(registry_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::query) : nullptr, gate)); // "Source ID of the Query API instance issuing the data Grain" // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/queryapi-subscriptions-websocket.html const nmos::id query_id = nmos::make_repeatable_id(nmos::experimental::fields::seed_id(registry_model.settings), U("/x-nmos/query")); auto& query_ws_api = registry_server.ws_handlers[{ {}, nmos::fields::query_ws_port(registry_model.settings) }]; - query_ws_api.first = nmos::make_query_ws_api(query_id, registry_model, query_ws_api.second, gate); + query_ws_api.first = nmos::make_query_ws_api(query_id, registry_model, query_ws_api.second, registry_implementation.ws_validate_authorization, gate); // Configure the Registration API - registry_server.api_routers[{ {}, nmos::fields::registration_port(registry_model.settings) }].mount({}, nmos::make_registration_api(registry_model, gate)); + registry_server.api_routers[{ {}, nmos::fields::registration_port(registry_model.settings) }].mount({}, nmos::make_registration_api(registry_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::registration) : nullptr, gate)); // Configure the Node API - registry_server.api_routers[{ {}, nmos::fields::node_port(registry_model.settings) }].mount({}, nmos::make_node_api(registry_model, {}, gate)); + registry_server.api_routers[{ {}, nmos::fields::node_port(registry_model.settings) }].mount({}, nmos::make_node_api(registry_model, {}, validate_authorization ? validate_authorization(nmos::experimental::scopes::node) : nullptr, gate)); // set up the node resources auto& self_resources = registry_model.node_resources; diff --git a/Development/nmos/registry_server.h b/Development/nmos/registry_server.h index 590c57c9d..82dce35ce 100644 --- a/Development/nmos/registry_server.h +++ b/Development/nmos/registry_server.h @@ -1,8 +1,10 @@ #ifndef NMOS_REGISTRY_SERVER_H #define NMOS_REGISTRY_SERVER_H +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" #include "nmos/ocsp_response_handler.h" +#include "nmos/ws_api_utils.h" namespace slog { @@ -23,11 +25,13 @@ namespace nmos // underlying implementation into the server instance for the NMOS Registry struct registry_implementation { - registry_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::ocsp_response_handler get_ocsp_response) + registry_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::ocsp_response_handler get_ocsp_response, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization) : load_server_certificates(std::move(load_server_certificates)) , load_dh_param(std::move(load_dh_param)) , load_ca_certificates(std::move(load_ca_certificates)) , get_ocsp_response(std::move(get_ocsp_response)) + , validate_authorization(std::move(validate_authorization)) + , ws_validate_authorization(std::move(ws_validate_authorization)) {} // use the default constructor and chaining member functions for fluent initialization @@ -39,6 +43,8 @@ namespace nmos registry_implementation& on_load_dh_param(nmos::load_dh_param_handler load_dh_param) { this->load_dh_param = std::move(load_dh_param); return *this; } registry_implementation& on_load_ca_certificates(nmos::load_ca_certificates_handler load_ca_certificates) { this->load_ca_certificates = std::move(load_ca_certificates); return *this; } registry_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } + registry_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return* this; } + registry_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } // determine if the required callbacks have been specified bool valid() const @@ -51,6 +57,9 @@ namespace nmos nmos::load_ca_certificates_handler load_ca_certificates; nmos::ocsp_response_handler get_ocsp_response; + + validate_authorization_handler validate_authorization; + ws_validate_authorization_handler ws_validate_authorization; }; // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API, the IS-10 Authorization API diff --git a/Development/nmos/resource.h b/Development/nmos/resource.h index 81eb0f558..aeacb620c 100644 --- a/Development/nmos/resource.h +++ b/Development/nmos/resource.h @@ -24,7 +24,7 @@ namespace nmos // when any data is modified, the update timestamp must be set, and resource events should be generated // *or more accurately, after insertion into the registry - resource(api_version version, type type, web::json::value&& data, nmos::id id, bool never_expire) + resource(api_version version, type type, web::json::value&& data, nmos::id id, bool never_expire, utility::string_t client_id = {}) : version(version) , downgrade_version() , type(type) @@ -33,10 +33,11 @@ namespace nmos , created(tai_now()) , updated(created) , health(never_expire ? health_forever : created.seconds) + , client_id(std::move(client_id)) {} - resource(api_version version, type type, web::json::value data, bool never_expire) - : resource(version, type, std::move(data), fields::id(data), never_expire) + resource(api_version version, type type, web::json::value data, bool never_expire, utility::string_t client_id = {}) + : resource(version, type, std::move(data), fields::id(data), never_expire, std::move(client_id)) {} // the API version of the Node API, Registration API or Query API exposing this resource @@ -69,6 +70,11 @@ namespace nmos // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating mutable details::copyable_atomic health; + + // Registry MUST register the Client ID of the client performing the registration. Subsequent requests to modify or delete a registered + // resource MUST validate the Client ID to ensure that clients do not, maliciously or incorrectly, alter resources belonging to other nodes + // see https://specs.amwa.tv/bcp-003-02/releases/v1.0.0/docs/1.0._Authorization_Practice.html#registry-client-authorization + utility::string_t client_id; }; namespace details diff --git a/Development/nmos/scope.h b/Development/nmos/scope.h new file mode 100644 index 000000000..1f3999531 --- /dev/null +++ b/Development/nmos/scope.h @@ -0,0 +1,48 @@ +#ifndef NMOS_SCOPE_H +#define NMOS_SCOPE_H + +#include "nmos/string_enum.h" + +namespace nmos +{ + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + // scope (used in JWT) + DEFINE_STRING_ENUM(scope) + namespace scopes + { + // IS-04 + const scope registration{ U("registration") }; + const scope query{ U("query") }; + const scope node{ U("node") }; + // IS-05 + const scope connection{ U("connection") }; + // IS-06 + const scope netctrl{ U("netctrl") }; + // IS-07 + const scope events{ U("events") }; + // IS-08 + const scope channelmapping{ U("channelmapping") }; + } + + inline utility::string_t make_scope(const scope& scope) + { + return scope.name; + } + + inline scope parse_scope(const utility::string_t& scope) + { + if (scopes::registration.name == scope) { return scopes::registration; } + if (scopes::query.name == scope) { return scopes::query; } + if (scopes::node.name == scope) { return scopes::node; } + if (scopes::connection.name == scope) { return scopes::connection; } + if (scopes::netctrl.name == scope) { return scopes::netctrl; } + if (scopes::events.name == scope) { return scopes::events; } + if (scopes::channelmapping.name == scope) { return scopes::channelmapping; } + return{}; + } + } +} + +#endif diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index c8943c93e..78a8e7ab1 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -81,6 +81,8 @@ namespace nmos if (registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::admin_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::mdns_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::schemas_port, http_port)); + web::json::insert(settings, std::make_pair(nmos::experimental::fields::authorization_redirect_port, http_port)); + web::json::insert(settings, std::make_pair(nmos::experimental::fields::jwks_uri_port, http_port)); } } } diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 62be9c25a..0523012f0 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -101,13 +101,20 @@ namespace nmos // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is09_versions{ U("is09_versions") }; // when omitted, nmos::is09_versions::all is used + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + const web::json::field_as_array is10_versions{ U("is10_versions") }; // when omitted, nmos::is10_versions::all is used + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely const web::json::field_as_integer_or pri{ U("pri"), 100 }; // default to highest_development_priority - // highest_pri, lowest_pri [node]: used to specify the (inclusive) range of suitable 'pri' values of discovered APIs, to avoid development and live systems colliding + // highest_pri, lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered APIs, to avoid development and live systems colliding const web::json::field_as_integer_or highest_pri{ U("highest_pri"), 0 }; // default to highest_active_priority; specifying no_priority disables discovery completely const web::json::field_as_integer_or lowest_pri{ U("lowest_pri"), (std::numeric_limits::max)() }; // default to no_priority + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + const web::json::field_as_integer_or authorization_highest_pri{ U("authorization_highest_pri"), 0 }; // default to highest_active_priority; specifying no_priority disables discovery completely + const web::json::field_as_integer_or authorization_lowest_pri{ U("authorization_lowest_pri"), (std::numeric_limits::max)() }; // default to no_priority + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances // e.g. Registration APIs, System APIs, or OCSP servers const web::json::field_as_integer_or discovery_backoff_min{ U("discovery_backoff_min"), 1 }; @@ -347,6 +354,76 @@ namespace nmos // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server const web::json::field_as_integer_or ocsp_request_max{ U("ocsp_request_max"), 30 }; + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + const web::json::field_as_string_or authorization_selector{ U("authorization_selector"), U("") }; + + // authorization_address [registry, node]: IP address or host name used to construct request URLs for Authorization APIs (if not discovered via DNS-SD) + const web::json::field_as_string authorization_address{ U("authorization_address") }; + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + const web::json::field_as_integer_or authorization_port{ U("authorization_port"), 443 }; + + // authorization_version [registry, node]: used to construct request URLs for authorization APIs (if not discovered via DNS-SD) + const web::json::field_as_string_or authorization_version{ U("authorization_version"), U("v1.0") }; + + // authorization_services [registry, node]: the discovered list of Authorization APIs, in the order they should be used + // this list is created and maintained by nmos::authorization_operation_thread; each entry is a uri like http://example.api.com/x-nmos/auth/{version} + const web::json::field_as_value authorization_services{ U("authorization_services") }; + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + const web::json::field_as_integer_or authorization_request_max{ U("authorization_request_max"), 30 }; + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + const web::json::field_as_integer_or fetch_authorization_public_keys_interval_min{ U("fetch_authorization_public_keys_interval_min"), 3600 }; + const web::json::field_as_integer_or fetch_authorization_public_keys_interval_max{ U("fetch_authorization_public_keys_interval_max"), 3660 }; + + // access_token_refresh_interval [node]: time interval (in seconds) to refresh access token from Authorization Server + // It specified the access token refresh period otherwise Bearer token's expires_in is used instead. + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#access-token-lifetime + const web::json::field_as_integer_or access_token_refresh_interval{ U("access_token_refresh_interval"), -1 }; + + // client_authorization [node]: whether clients should use authorization to access protected APIs + const web::json::field_as_bool_or client_authorization{ U("client_authorization"), false }; + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + const web::json::field_as_bool_or server_authorization{ U("server_authorization"), false }; + + // authorization_code_flow_max [node]: timeout for the authorization code workflow (in seconds) + // No timeout if value is set to -1, default to 30 seconds + const web::json::field_as_integer_or authorization_code_flow_max{ U("authorization_code_flow_max"), 30 }; + + // authorization_flow [node]: used to specify the authorization flow for the registered scopes + // supported flow are authorization_code and client_credentials + // client_credentials SHOULD only be used for NO user interface node/registry, otherwise authorization_code MUST be used + const web::json::field_as_string_or authorization_flow{ U("authorization_flow"), U("authorization_code") }; + + // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration + const web::json::field_as_integer_or authorization_redirect_port{ U("authorization_redirect_port"), 3218 }; + + // initial_access_token [node]: initial access token giving access to the client registration endpoint for non-opened registration + const web::json::field_as_string_or initial_access_token{ U("initial_access_token"), U("") }; + + // authorization_scopes [node]: used to specify the supported scopes for client registration + // supported scopes are registration, query, node, connection, events and channelmapping + const web::json::field_as_array authorization_scopes{ U("authorization_scopes") }; + + // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint + // supported methods are none, client_secret_basic and private_key_jwt, default to client_secret_basic, where none is used for public client + const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic")}; + + // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration + const web::json::field_as_integer_or jwks_uri_port{ U("jwks_uri_port"), 3219 }; + + // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) + const web::json::field_as_bool_or validate_openid_client{ U("validate_openid_client"), true }; + + // no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks + // as it is because not all Authorization server can cope with URL with trailing dot, default to true + const web::json::field_as_bool_or no_trailing_dot_for_authorization_callback_uri{ U("no_trailing_dot_for_authorization_callback_uri"), true}; } } } diff --git a/Development/nmos/slog.h b/Development/nmos/slog.h index d3fc150fd..d3bd48bee 100644 --- a/Development/nmos/slog.h +++ b/Development/nmos/slog.h @@ -44,6 +44,7 @@ namespace nmos const category send_events_ws_commands{ "send_events_ws_commands" }; const category node_system_behaviour{ "node_system_behaviour" }; const category ocsp_behaviour{ "ocsp_behaviour" }; + const category authorization_behaviour{ "authorization_behaviour" }; // other categories may be defined ad-hoc } diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp new file mode 100644 index 000000000..d66c3a6d2 --- /dev/null +++ b/Development/nmos/ws_api_utils.cpp @@ -0,0 +1,52 @@ +#include "nmos/ws_api_utils.h" + +#include "cpprest/http_utils.h" +#include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_state.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + // callbacks from this function are called with the model locked, and may read or write directly to the model + ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, slog::base_gate& gate) + { + return [&model, &authorization_state, &gate](web::http::http_request& request, const nmos::experimental::scope& scope) + { + if (web::http::methods::OPTIONS != request.method() && nmos::experimental::fields::server_authorization(model.settings)) + { + const auto& settings = model.settings; + + authorization_state.write_lock(); + const auto error = ws_validate_authorization(authorization_state.issuers, request, scope, nmos::get_host_name(settings), authorization_state.authorization_server_uri, authorization_state.token_issuer, gate); + if (error) + { + // set error repsonse + auto realm = web::http::get_host_port(request).first; + if (realm.empty()) { realm = nmos::get_host(settings); } + web::http::http_response res; + nmos::experimental::details::set_error_reply(res, realm, error); + request.reply(res); + + // if error was deal to no matching keys, trigger authorization_token_issuer_thread to fetch public keys from the token issuer + if (error.value == authorization_error::no_matching_keys) + { + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << error.message; + authorization_state.fetch_token_issuer_pubkeys = true; + model.notify(); + } + else + { + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << error.message; + } + return false; + } + } + return true; + }; + } + } +} diff --git a/Development/nmos/ws_api_utils.h b/Development/nmos/ws_api_utils.h new file mode 100644 index 000000000..b45e56e44 --- /dev/null +++ b/Development/nmos/ws_api_utils.h @@ -0,0 +1,26 @@ +#ifndef NMOS_WS_API_UTILS_H +#define NMOS_WS_API_UTILS_H + +#include +#include "cpprest/http_msg.h" +#include "nmos/scope.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + typedef std::function ws_validate_authorization_handler; + ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/third_party/README.md b/Development/third_party/README.md index 2788ca9e0..4f347ee3f 100644 --- a/Development/third_party/README.md +++ b/Development/third_party/README.md @@ -6,6 +6,8 @@ Third-party source files used by the nmos-cpp libraries The [Catch](https://github.com/philsquared/Catch) (automated test framework) single header version - [cmake](cmake) CMake modules derived from third-party sources +- [jwt-cpp](jwt-cpp) + The [C++ JSON Web Tokens creator and validator] (https://github.com/Thalhammer/jwt-cpp) header only library - [mDNSResponder](mDNSResponder) Patches and patched source files for the Bonjour DNS-SD implementation - [nlohmann](nlohmann) @@ -18,5 +20,7 @@ Third-party source files used by the nmos-cpp libraries The JSON Schema files used for validation of Channel Mapping API requests and responses - [is-09](is-09) The JSON Schema files used for validation of System API requests and responses +- [is-10](is-10) + The JSON Schema files used for validation of Authorization API requests and responses - [WpdPack](WpdPack) Libraries and header files from the [WinPcap](https://www.winpcap.org/) Developer's Pack diff --git a/Development/third_party/is-10/README.md b/Development/third_party/is-10/README.md new file mode 100644 index 000000000..3360f110b --- /dev/null +++ b/Development/third_party/is-10/README.md @@ -0,0 +1,23 @@ +# \[Work In Progress\] AMWA IS-10 NMOS Authorization Specification + +AMWA IS-10 specifies an API for the authorization of other NMOS APIs. + +## Getting started + +Readers are advised to be familiar with: + +- The [overview of Networked Media Open Specifications](https://github.com/AMWA-TV/nmos) +- [AMWA BCP-003](https://github.com/AMWA-TV/nmos-api-security) Best Practices for NMOS API Security + +Readers should then read the [documentation](docs/) in this repository, and the [APIs](APIs/), which are written in RAML -- if a suitable tool is not available for reading this, then [this](APIs/generateHTML) will create HTML versions. + +> HTML rendered versions of all NMOS Specifications are available on the [NMOS GitHub pages](https://amwa-tv.github.io/nmos) + +## Contents + +- README.md -- This file +- [docs/](docs/) -- Documentation targeting those implementing APIs and clients. +- [APIs/](APIs/) -- Normative specifications of APIs +- [examples/](examples/) -- Example JSON requests and responses for APIs +- [LICENSE](LICENSE) -- Licenses for software and text documents +- [NOTICE](NOTICE) -- Disclaimer diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json new file mode 100644 index 000000000..79c68bb1c --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Authorization clients table", + "description": "Authorization client for the different Auhorization server", + "type": "array", + "items": { + "type": "object", + "required": ["authorization_server_uri", "client_metadata"], + "properties": { + "authorization_server_uri": { + "description": "The authorization server's issuer identifier, which is a URL that uses the 'https' scheme and has no query or fragment components.", + "format": "uri", + "type": "string", + "minLength": 1 + }, + "client_metadata" : { + "$ref": "register_client_response.json" + } + } + } +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json new file mode 100644 index 000000000..fbfddd81e --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "Authorization API metadata resource", + "description": "Displays the Authorization server metadata", + "properties": { + "issuer": { + "description": "The authorization server's issuer identifier, which is a URL that uses the 'https' scheme and has no query or fragment components. Authorization server metadata is published at a location that is '.well-known' according to RFC 5785 [RFC5785] derived from this issuer identifier, as described in Section 3. The issuer identifier is used to prevent authorization server mix-up attacks.", + "format": "uri", + "type": "string" + }, + "authorization_endpoint": { + "description": "URL of the authorization server's authorization endpoint [RFC6749]. This is REQUIRED unless no grant types are supported that use the authorization endpoint.", + "format": "uri", + "type": "string" + }, + "token_endpoint": { + "description": "URL of the authorization server's token endpoint [RFC6749]. This is REQUIRED unless only the implicit grant type is supported.", + "format": "uri", + "type": "string" + }, + "jwks_uri": { + "description": "URL of the authorization server's JWK Set [JWK] document. The referenced document contains the signing key(s) the client uses to validate signatures from the authorization server. This URL MUST use the 'https' scheme. The JWK Set MAY also contain the server's encryption key or keys, which are used by clients to encrypt requests to the server. When both signing and encryption keys are made available, a 'use' (public key use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage.", + "format": "uri", + "type": "string" + }, + "registration_endpoint": { + "description": "URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint [RFC7591].", + "format": "uri", + "type": "string" + }, + "scopes_supported": { + "description": "JSON array containing a list of the OAuth 2.0 [RFC6749] 'scope' values that this authorization server supports. Servers MAY choose not to advertise some supported scope values even when this parameter is used.", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "response_types_supported": { + "description": "JSON array containing a list of the OAuth 2.0 'response_type' values that this authorization server supports. The array values used are the same as those used with the 'response_types' parameter defined by 'OAuth 2.0 Dynamic Client Registration Protocol' in RFC7591", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "grant_types_supported": { + "description": "JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. The array values used are the same as those used with the 'grant_types' parameter defined by 'OAuth 2.0 Dynamic Client Registration Protocol' in [RFC7591]. If omitted, the default value is ['authorization_code', 'implicit']", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "revocation_endpoint": { + "description": "URL of the authorization server's OAuth 2.0 revocation endpoint in RFC7009.", + "format": "uri", + "type": "string" + }, + "code_challenge_methods_supported": { + "description": "JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server. Code challenge method values are used in the 'code_challenge_method' parameter defined in Section 4.3 of [RFC7636]. The valid code challenge method values are those registered in the IANA 'PKCE Code Challenge Methods' registry [IANA.OAuth.Parameters]. If omitted, the authorization server does not support PKCE.", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + } + }, + "required": [ + "issuer", + "authorization_endpoint", + "token_endpoint", + "jwks_uri", + "registration_endpoint", + "response_types_supported", + "code_challenge_methods_supported" + ] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json new file mode 100644 index 000000000..95eee6f90 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "JWKs Response", + "description": "JSON Web Key Set to validate Access Token", + "type": "object", + "allOf": [ + {"$ref": "jwks_schema.json"} + ] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json new file mode 100644 index 000000000..44804bee3 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "JSON Web Key Set", + "description": "JSON Web Key Set to validate JSON Web Token", + "type": "object", + "properties": { + "keys": { + "description": "The value of the 'keys' parameter is an array of JWK values. By default, the order of the JWK values within the array does not imply an order of preference among them, although applications of JWK Sets can choose to assign a meaning to the order for their purposes, if desired.", + "type": "array", + "items": { + "type": "object", + "properties": { + "kty": { + "type": "string" + }, + "use": { + "type": "string" + }, + "key_ops": { + "type": "string" + }, + "alg": { + "type": "string" + }, + "kid": { + "type": "string" + }, + "x5u": { + "type": "string", + "format": "uri" + }, + "x5c": { + "type": "array", + "items": { + "type": "string" + } + }, + "x5t": { + "type": "string" + }, + "x5t#S256": { + "type": "string" + } + }, + "required": ["kty"] + } + } + }, + "required": ["keys"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json new file mode 100644 index 000000000..dcc313c8f --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Client Registration Error Response", + "description": "Describes the client registration endpoint's OAuth error response", + "type": "object", + "minItems": 1, + "properties": { + "error": { + "description": "Error Type", + "type": "string", + "enum": ["invalid_redirect_uri", "invalid_client_metadata", "invalid_software_statement", "unapproved_software_statement"] + }, + "error_description": { + "description": "Human-readable ASCII text providing additional information", + "type": "string" + }, + "error_uri": { + "description": "A URI identifying a human-readable web page with information about the error", + "type": "string", + "format": "uri" + } + }, + "required": ["error"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json new file mode 100644 index 000000000..7fcfcc7d2 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Register Client Request", + "description": "Object defining client registration request", + "type": "object", + "properties": { + "redirect_uris": { + "description": "Array of redirection URI strings for use in redirect-based flows such as the authorization code and implicit flows", + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint_auth_method": { + "description": "String indicator of the requested authentication method for the token endpoint", + "type": "string" + }, + "grant_types": { + "description": "Array of OAuth 2.0 grant type strings that the client can use at the token endpoint", + "type": "array", + "items": { + "type": "string" + } + }, + "response_types": { + "description": "Array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint", + "type": "array", + "items": { + "type": "string" + } + }, + "client_name": { + "description": "Human-readable string name of the client to be presented to the end-user during authorization", + "type": "string" + }, + "client_uri": { + "description": "URL string of a web page providing information about the client", + "type": "string" + }, + "logo_uri": { + "description": "URL string that references a logo for the client", + "type": "string" + }, + "scope": { + "description": "String containing a space-separated list of scope values", + "type": "string" + }, + "contacts": { + "description": "Array of strings representing ways to contact people responsible for this client, typically email addresses", + "type": "array", + "items": { + "type": "string" + } + }, + "tos_uri": { + "description": "URL string that points to a human-readable terms of service document for the client", + "type": "string" + }, + "policy_uri": { + "description": "URL string that points to a human-readable privacy policy document", + "type": "string" + }, + "jwks_uri": { + "description": "URL string referencing the client's JSON Web Key (JWK) Set document, which contains the client's public keys", + "type": "string" + }, + "jwks": { + "description": "Client's JSON Web Key Set document value, which contains the client's public keys", + "type": "object", + "allOf": [ + {"$ref": "jwks_schema.json"} + ] + }, + "software_id": { + "description": "A unique identifier string (e.g. a UUID) assigned by the client developer or software publisher", + "type": "string" + }, + "software_version": { + "description": "A version identifier string for the client software identified by 'software_id'", + "type": "string" + } + }, + "required": [ "client_name" ] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json new file mode 100644 index 000000000..ee54d7d35 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Register Client Response", + "description": "Object defining successful client registration", + "type": "object", + "properties": { + "client_id": { + "description": "OAuth 2.0 client identifier string", + "type": "string" + }, + "client_secret": { + "description": "OAuth 2.0 client secret string", + "type": "string" + }, + "client_id_issued_at": { + "description": "UTC time at which the client identifier was issued", + "type": "number" + }, + "client_secret_expires_at": { + "description": "Time at which the client secret will expire or 0 if it will not expire", + "type": "number" + }, + "redirect_uris": { + "description": "Array of redirection URI strings for use in redirect-based flows such as the authorization code and implicit flows", + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint_auth_method": { + "description": "String indicator of the requested authentication method for the token endpoint", + "type": "string", + "default": "client_secret_basic" + }, + "grant_types": { + "description": "Array of OAuth 2.0 grant type strings that the client can use at the token endpoint", + "type": "array", + "items": { + "type": "string" + }, + "default": [ "authorization_code" ] + }, + "response_types": { + "description": "Array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint", + "type": "array", + "items": { + "type": "string" + }, + "default": [ "code" ] + }, + "client_name": { + "description": "Human-readable string name of the client to be presented to the end-user during authorization", + "type": "string" + }, + "client_uri": { + "description": "URL string of a web page providing information about the client", + "type": "string" + }, + "logo_uri": { + "description": "URL string that references a logo for the client", + "type": "string" + }, + "scope": { + "description": "String containing a space-separated list of scope values", + "type": "string" + }, + "contacts": { + "description": "Array of strings representing ways to contact people responsible for this client, typically email addresses", + "type": "array", + "items": { + "type": "string" + } + }, + "tos_uri": { + "description": "URL string that points to a human-readable terms of service document for the client", + "type": "string" + }, + "policy_uri": { + "description": "URL string that points to a human-readable privacy policy document", + "type": "string" + }, + "jwks_uri": { + "description": "URL string referencing the client's JSON Web Key (JWK) Set document, which contains the client's public keys", + "type": "string" + }, + "jwks": { + "description": "Client's JSON Web Key Set document value, which contains the client's public keys", + "type": "object", + "allOf": [ + {"$ref": "jwks_schema.json"} + ] + }, + "software_id": { + "description": "A unique identifier string (e.g. a UUID) assigned by the client developer or software publisher", + "type": "string" + }, + "software_version": { + "description": "A version identifier string for the client software identified by 'software_id'", + "type": "string" + } + }, + "required": ["client_id"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json new file mode 100644 index 000000000..a07584eec --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Token Error Response", + "description": "Describes the token endpoint's OAuth error response", + "type": "object", + "minItems": 1, + "properties": { + "error": { + "description": "Error Type", + "type": "string", + "enum": ["invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "unsupported_grant_type", "invalid_scope", "unsupported_token_type"] + }, + "error_description": { + "description": "Human-readable ASCII text providing additional information", + "type": "string" + }, + "error_uri": { + "description": "A URI identifying a human-readable web page with information about the error", + "type": "string", + "format": "uri" + } + }, + "required": ["error"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json new file mode 100644 index 000000000..636cfd116 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Token Response", + "description": "OAuth2 Response for the request of a Bearer Token", + "type": "object", + "properties": { + "access_token": { + "description": "Access Token to be used in accessing protected endpoints", + "type": "string" + }, + "expires_in": { + "description": "The lifetime in seconds of the Access Token", + "type": "integer" + }, + "refresh_token": { + "description": "Refresh Token to be used to obtain further Access Tokens", + "type": "string" + }, + "scope": { + "description": "The scope of the Access Token", + "type": "string" + }, + "token_type": { + "description": "The type of the Token issued", + "type": "string" + } + }, + "required": ["access_token", "expires_in", "token_type"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json new file mode 100644 index 000000000..2a6a66218 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "JSON Web Token Contents", + "description": "Claims contained within JSON Web Token", + "type": "object", + "properties": { + "iss": { + "description": "A case-sensitive string containing a StringOrURI value that identifies the Authorization Server that issued the JWT", + "type": "string" + }, + "sub": { + "description": "The unique identifier assigned to the end-user by the user authorization system", + "type": "string" + }, + "aud": { + "description": "A JSON array of case-sensitive strings, each containing a StringOrURI value that identifies the recipients that the JWT is intended for", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "exp": { + "description": "The UTC time at which the token expires", + "type": "number" + }, + "iat": { + "description": "The UTC time at which the token was issued", + "type": "number" + }, + "client_id": { + "description": "The client identifier of the OAuth 2.0 client that requested the token", + "type": "string" + }, + "azp": { + "description": "The client identifier of the OAuth 2.0 client that requested the token", + "type": "string" + }, + "scope": { + "description": "A string containing a space-separated list of scopes associated with the token", + "type": "string" + } + }, + "patternProperties": { + "^x-nmos-[a-z]+$": { + "description": "An object containing the access permissions of the user for the NMOS API identified by this attribute's name", + "type": "object", + "minProperties": 1, + "properties": { + "read": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, + "write": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + } + } + } + }, + "required": ["iss", "sub", "aud", "exp"] +} diff --git a/Development/third_party/jwt-cpp/README.md b/Development/third_party/jwt-cpp/README.md new file mode 100644 index 000000000..5e3903262 --- /dev/null +++ b/Development/third_party/jwt-cpp/README.md @@ -0,0 +1,208 @@ +# ![logo](https://raw.githubusercontent.com/Thalhammer/jwt-cpp/master/.github/logo.svg) + +[![License Badge](https://img.shields.io/github/license/Thalhammer/jwt-cpp)](https://github.com/Thalhammer/jwt-cpp/blob/master/LICENSE) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/5f7055e294744901991fd0a1620b231d)](https://app.codacy.com/app/Thalhammer/jwt-cpp?utm_source=github.com&utm_medium=referral&utm_content=Thalhammer/jwt-cpp&utm_campaign=Badge_Grade_Settings) +[![Linux Badge][Linux]][Cross-Platform] +[![MacOS Badge][MacOS]][Cross-Platform] +[![Windows Badge][Windows]][Cross-Platform] +[![Coverage Status](https://coveralls.io/repos/github/Thalhammer/jwt-cpp/badge.svg?branch=master)](https://coveralls.io/github/Thalhammer/jwt-cpp?branch=master) +[![Documentation Badge](https://img.shields.io/badge/Documentation-master-blue)](https://thalhammer.github.io/jwt-cpp/) +[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/Thalhammer/jwt-cpp?include_prereleases)](https://github.com/Thalhammer/jwt-cpp/releases) +[![Stars Badge](https://img.shields.io/github/stars/Thalhammer/jwt-cpp)](https://github.com/Thalhammer/jwt-cpp/stargazers) + +[Linux]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/cross-platform/ubuntu-latest/shields.json +[MacOS]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/cross-platform/macos-latest/shields.json +[Windows]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/cross-platform/windows-latest/shields.json +[Cross-Platform]: https://github.com/Thalhammer/jwt-cpp/actions?query=workflow%3A%22Cross-Platform+CI%22 + +A header only library for creating and validating [JSON Web Tokens](https://tools.ietf.org/html/rfc7519) in C++11. For a great introduction, [read this](https://jwt.io/introduction/). + +## Signature algorithms + +jwt-cpp supports all the algorithms defined by the specifications. The modular design allows to easily add additional algorithms without any problems. If you need any feel free to create a pull request or [open an issue](https://github.com/Thalhammer/jwt-cpp/issues/new). + +For completeness, here is a list of all supported algorithms: + +| HMSC | RSA | ECDSA | PSS | EdDSA | +| ----- | ----- | ----- | ----- | ------- | +| HS256 | RS256 | ES256 | PS256 | Ed25519 | +| HS384 | RS384 | ES384 | PS384 | Ed448 | +| HS512 | RS512 | ES512 | PS512 | | + +## SSL Compatibility + +In the name of flexibility and extensibility, jwt-cpp supports both [OpenSSL](https://github.com/openssl/openssl) and [LibreSSL](https://github.com/libressl-portable/portable). These are the version which are, or have been, tested: + +| OpenSSL | LibreSSL | +| -------------- | --------------- | +| [1.0.2][1.0.2] | ![3.1.5][3.1] | +| 1.1.0 | ![3.2.3][3.2] | +| [1.1.1][1.1.1] | ![3.3.1][3.3] | + +[1.0.2]: https://travis-ci.com/github/Thalhammer/jwt-cpp +[1.1.1]: https://github.com/Thalhammer/jwt-cpp/actions?query=workflow%3A%22Coverage+CI%22 +[3.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.1.5/shields.json +[3.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.2.3/shields.json +[3.3]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.3.1/shields.json + +## Overview + +There is no hard dependency on a JSON library. Instead, there's a generic `jwt::basic_claim` which is templated around type traits, which described the semantic [JSON types](https://json-schema.org/understanding-json-schema/reference/type.html) for a value, object, array, string, number, integer and boolean, as well as methods to translate between them. + +```cpp +jwt::basic_claim claim(json::object({{"json", true},{"example", 0}})); +``` + +This allows for complete freedom when picking which libraries you want to use. For more information, [see below](#providing-your-own-json-traits-your-traits). + +In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. + +As for the base64 requirements of JWTs, this libary provides `base.h` with all the required implentation; However base64 implementations are very common, with varying degrees of performance. When providing your own base64 implementation, you can define `JWT_DISABLE_BASE64` to remove the jwt-cpp implementation. + +### Getting Started + +Simple example of decoding a token and printing all [claims](https://tools.ietf.org/html/rfc7519#section-4) ([try it out](https://github.com/Thalhammer/jwt-cpp/tree/master/example/print-claims.cpp)): + +```cpp +#include +#include + +int main() { + std::string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; + auto decoded = jwt::decode(token); + + for(auto& e : decoded.get_payload_claims()) + std::cout << e.first << " = " << e.second << std::endl; +} +``` + +In order to verify a token you first build a verifier and use it to verify a decoded token. + +```cpp +auto verifier = jwt::verify() + .allow_algorithm(jwt::algorithm::hs256{ "secret" }) + .with_issuer("auth0"); + +verifier.verify(decoded_token); +``` + +The created verifier is stateless so you can reuse it for different tokens. + +Creating a token (and signing) is equally as easy. + +```cpp +auto token = jwt::create() + .set_issuer("auth0") + .set_type("JWS") + .set_payload_claim("sample", jwt::claim(std::string("test"))) + .sign(jwt::algorithm::hs256{"secret"}); +``` + +Here is a simple example of creating a token that will expire in one hour: + +```cpp +auto token = jwt::create() + .set_issuer("auth0") + .set_issued_at(std::chrono::system_clock::now()) + .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{3600}) + .sign(jwt::algorithm::hs256{"secret"}); +``` + +> To see more examples working with RSA public and private keys, visit our [examples](https://github.com/Thalhammer/jwt-cpp/tree/master/example)! + +### Providing your own JSON Traits + +There are several key items that need to be provided to a `jwt::basic_claim` in order for it to be interoptable with you JSON library of choice. + +* type specifications +* conversion from generic "value type" to a specific type +* serialization and parsing + +If ever you are not sure, the traits are heavily checked against static asserts to make sure you provide everything that's required. + +> :warning: Not all JSON libraries are a like, you may need to extent certain types such that it can be used by jwt-cpp. See this [example](https://github.com/Thalhammer/jwt-cpp/blob/ac3de9e69bc698a464dacb256a1b50512843f092/tests/jsoncons/JsonconsTest.cpp). + +```cpp +struct my_favorite_json_library_traits { + // Type Specifications + using value_type = json; // The generic "value type" implementation, most libraries have one + using object_type = json::object_t; // The "map type" string to value + using array_type = json::array_t; // The "list type" array of values + using string_type = std::string; // The "list of chars", must be a narrow char + using number_type = double; // The "percision type" + using integer_type = int64_t; // The "integral type" + using boolean_type = bool; // The "boolean type" + + // Translation between the implementation notion of type, to the jwt::json::type equivilant + static jwt::json::type get_type(const value_type &val) { + using jwt::json::type; + + if (val.type() == json::value_t::object) + return type::object; + if (val.type() == json::value_t::array) + return type::array; + if (val.type() == json::value_t::string) + return type::string; + if (val.type() == json::value_t::number_float) + return type::number; + if (val.type() == json::value_t::number_integer) + return type::integer; + if (val.type() == json::value_t::boolean) + return type::boolean; + + throw std::logic_error("invalid type"); + } + + // Conversion from generic value to specific type + static object_type as_object(const value_type &val); + static array_type as_array(const value_type &val); + static string_type as_string(const value_type &val); + static number_type as_number(const value_type &val); + static integer_type as_int(const value_type &val); + static boolean_type as_bool(const value_type &val); + + // serilization and parsing + static bool parse(value_type &val, string_type str); + static string_type serialize(const value_type &val); // with no extra whitespace, padding or indentation +}; +``` + +## Contributing + +If you have an improvement or found a bug feel free to [open an issue](https://github.com/Thalhammer/jwt-cpp/issues/new) or add the change and create a pull request. If you file a bug please make sure to include as much information about your environment (compiler version, etc.) as possible to help reproduce the issue. If you add a new feature please make sure to also include test cases for it. + +## Dependencies + +In order to use jwt-cpp you need the following tools. + +* libcrypto (openssl or compatible) +* libssl-dev (for the header files) +* a compiler supporting at least c++11 +* basic stl support + +In order to build the test cases you also need + +* gtest +* pthread + +## Troubleshooting + +### Expired tokens + +If you are generating tokens that seem to immediately expire, you are likely not using UTC. Specifically, +if you use `get_time` to get the current time, it likely uses localtime, while this library uses UTC, +which may be why your token is immediately expiring. Please see example above on the right way to use current time. + +### Missing \_HMAC and \_EVP_sha256 symbols on Mac + +There seems to exists a problem with the included openssl library of MacOS. Make sure you link to one provided by brew. +See [here](https://github.com/Thalhammer/jwt-cpp/issues/6) for more details. + +### Building on windows fails with syntax errors + +The header ``, which is often included in windowsprojects, defines macros for MIN and MAX which screw up std::numeric_limits. +See [here](https://github.com/Thalhammer/jwt-cpp/issues/5) for more details. To fix this do one of the following things: + +* define NOMINMAX, which suppresses this behaviour +* include this library before you include windows.h +* place `#undef max` and `#undef min` before you include this library diff --git a/Development/third_party/jwt-cpp/base.h b/Development/third_party/jwt-cpp/base.h new file mode 100644 index 000000000..c447113c9 --- /dev/null +++ b/Development/third_party/jwt-cpp/base.h @@ -0,0 +1,208 @@ +#ifndef JWT_CPP_BASE_H +#define JWT_CPP_BASE_H + +#include +#include +#include + +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(fallthrough) +#define JWT_FALLTHROUGH [[fallthrough]] +#endif +#endif + +#ifndef JWT_FALLTHROUGH +#define JWT_FALLTHROUGH +#endif + +namespace jwt { + /** + * \brief character maps when encoding and decoding + */ + namespace alphabet { + /** + * \brief valid list of characted when working with [Base64](https://tools.ietf.org/html/rfc3548) + */ + struct base64 { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}}; + return data; + } + static const std::string& fill() { + static std::string fill{"="}; + return fill; + } + }; + /** + * \brief valid list of characted when working with [Base64URL](https://tools.ietf.org/html/rfc4648) + */ + struct base64url { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; + return data; + } + static const std::string& fill() { + static std::string fill{"%3d"}; + return fill; + } + }; + } // namespace alphabet + + /** + * \brief Alphabet generic methods for working with encoding/decoding the base64 family + */ + class base { + public: + template + static std::string encode(const std::string& bin) { + return encode(bin, T::data(), T::fill()); + } + template + static std::string decode(const std::string& base) { + return decode(base, T::data(), T::fill()); + } + template + static std::string pad(const std::string& base) { + return pad(base, T::fill()); + } + template + static std::string trim(const std::string& base) { + return trim(base, T::fill()); + } + + private: + static std::string encode(const std::string& bin, const std::array& alphabet, + const std::string& fill) { + size_t size = bin.size(); + std::string res; + + // clear incomplete bytes + size_t fast_size = size - size % 3; + for (size_t i = 0; i < fast_size;) { + uint32_t octet_a = static_cast(bin[i++]); + uint32_t octet_b = static_cast(bin[i++]); + uint32_t octet_c = static_cast(bin[i++]); + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += alphabet[(triple >> 0 * 6) & 0x3F]; + } + + if (fast_size == size) return res; + + size_t mod = size % 3; + + uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + switch (mod) { + case 1: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += fill; + res += fill; + break; + case 2: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += fill; + break; + default: break; + } + + return res; + } + + static std::string decode(const std::string& base, const std::array& alphabet, + const std::string& fill) { + size_t size = base.size(); + + size_t fill_cnt = 0; + while (size > fill.size()) { + if (base.substr(size - fill.size(), fill.size()) == fill) { + fill_cnt++; + size -= fill.size(); + if (fill_cnt > 2) throw std::runtime_error("Invalid input"); + } else + break; + } + + if ((size + fill_cnt) % 4 != 0) throw std::runtime_error("Invalid input"); + + size_t out_size = size / 4 * 3; + std::string res; + res.reserve(out_size); + + auto get_sextet = [&](size_t offset) { + for (size_t i = 0; i < alphabet.size(); i++) { + if (alphabet[i] == base[offset]) return static_cast(i); + } + throw std::runtime_error("Invalid input"); + }; + + size_t fast_size = size - size % 4; + for (size_t i = 0; i < fast_size;) { + uint32_t sextet_a = get_sextet(i++); + uint32_t sextet_b = get_sextet(i++); + uint32_t sextet_c = get_sextet(i++); + uint32_t sextet_d = get_sextet(i++); + + uint32_t triple = (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); + + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + res += static_cast((triple >> 0 * 8) & 0xFFU); + } + + if (fill_cnt == 0) return res; + + uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); + + switch (fill_cnt) { + case 1: + triple |= (get_sextet(fast_size + 2) << 1 * 6); + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + break; + case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; + default: break; + } + + return res; + } + + static std::string pad(const std::string& base, const std::string& fill) { + std::string padding; + switch (base.size() % 4) { + case 1: padding += fill; JWT_FALLTHROUGH; + case 2: padding += fill; JWT_FALLTHROUGH; + case 3: padding += fill; JWT_FALLTHROUGH; + default: break; + } + + return base + padding; + } + + static std::string trim(const std::string& base, const std::string& fill) { + auto pos = base.find(fill); + return base.substr(0, pos); + } + }; +} // namespace jwt + +#endif diff --git a/Development/third_party/jwt-cpp/jwt.h b/Development/third_party/jwt-cpp/jwt.h new file mode 100644 index 000000000..edd08912b --- /dev/null +++ b/Development/third_party/jwt-cpp/jwt.h @@ -0,0 +1,3040 @@ +#ifndef JWT_CPP_JWT_H +#define JWT_CPP_JWT_H + +#ifndef JWT_DISABLE_PICOJSON +#ifndef PICOJSON_USE_INT64 +#define PICOJSON_USE_INT64 +#endif +#include "picojson/picojson.h" +#endif + +#ifndef JWT_DISABLE_BASE64 +#include "base.h" +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if __cplusplus >= 201402L +#ifdef __has_include +#if __has_include() +#include +#endif +#endif +#endif + +// If openssl version less than 1.1 +#if OPENSSL_VERSION_NUMBER < 0x10100000L +#define OPENSSL10 +#endif + +// If openssl version less than 1.1.1 +#if OPENSSL_VERSION_NUMBER < 0x10101000L +#define OPENSSL110 +#endif + +#if defined(LIBRESSL_VERSION_NUMBER) +#define OPENSSL10 +#define OPENSSL110 +#endif + +#ifndef JWT_CLAIM_EXPLICIT +#define JWT_CLAIM_EXPLICIT explicit +#endif + +/** + * \brief JSON Web Token + * + * A namespace to contain everything related to handling JSON Web Tokens, JWT for short, + * as a part of [RFC7519](https://tools.ietf.org/html/rfc7519), or alternatively for + * JWS (JSON Web Signature) from [RFC7515](https://tools.ietf.org/html/rfc7515) + */ +namespace jwt { + using date = std::chrono::system_clock::time_point; + + /** + * \brief Everything related to error codes issued by the library + */ + namespace error { + struct signature_verification_exception : public std::system_error { + using system_error::system_error; + }; + struct signature_generation_exception : public std::system_error { + using system_error::system_error; + }; + struct rsa_exception : public std::system_error { + using system_error::system_error; + }; + struct ecdsa_exception : public std::system_error { + using system_error::system_error; + }; + struct token_verification_exception : public std::system_error { + using system_error::system_error; + }; + /** + * \brief Errors related to processing of RSA signatures + */ + enum class rsa_error { + ok = 0, + cert_load_failed = 10, + get_key_failed, + write_key_failed, + write_cert_failed, + convert_to_pem_failed, + load_key_bio_write, + load_key_bio_read, + create_mem_bio_failed, + no_key_provided + }; + /** + * \brief Error category for RSA errors + */ + inline std::error_category& rsa_error_category() { + class rsa_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "rsa_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case rsa_error::ok: return "no error"; + case rsa_error::cert_load_failed: return "error loading cert into memory"; + case rsa_error::get_key_failed: return "error getting key from certificate"; + case rsa_error::write_key_failed: return "error writing key data in PEM format"; + case rsa_error::write_cert_failed: return "error writing cert data in PEM format"; + case rsa_error::convert_to_pem_failed: return "failed to convert key to pem"; + case rsa_error::load_key_bio_write: return "failed to load key: bio write failed"; + case rsa_error::load_key_bio_read: return "failed to load key: bio read failed"; + case rsa_error::create_mem_bio_failed: return "failed to create memory bio"; + case rsa_error::no_key_provided: return "at least one of public or private key need to be present"; + default: return "unknown RSA error"; + } + } + }; + static rsa_error_cat cat; + return cat; + } + + inline std::error_code make_error_code(rsa_error e) { return {static_cast(e), rsa_error_category()}; } + /** + * \brief Errors related to processing of RSA signatures + */ + enum class ecdsa_error { + ok = 0, + load_key_bio_write = 10, + load_key_bio_read, + create_mem_bio_failed, + no_key_provided, + invalid_key_size, + invalid_key + }; + /** + * \brief Error category for ECDSA errors + */ + inline std::error_category& ecdsa_error_category() { + class ecdsa_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "ecdsa_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case ecdsa_error::ok: return "no error"; + case ecdsa_error::load_key_bio_write: return "failed to load key: bio write failed"; + case ecdsa_error::load_key_bio_read: return "failed to load key: bio read failed"; + case ecdsa_error::create_mem_bio_failed: return "failed to create memory bio"; + case ecdsa_error::no_key_provided: + return "at least one of public or private key need to be present"; + case ecdsa_error::invalid_key_size: return "invalid key size"; + case ecdsa_error::invalid_key: return "invalid key"; + default: return "unknown ECDSA error"; + } + } + }; + static ecdsa_error_cat cat; + return cat; + } + + inline std::error_code make_error_code(ecdsa_error e) { return {static_cast(e), ecdsa_error_category()}; } + + /** + * \brief Errors related to verification of signatures + */ + enum class signature_verification_error { + ok = 0, + invalid_signature = 10, + create_context_failed, + verifyinit_failed, + verifyupdate_failed, + verifyfinal_failed, + get_key_failed + }; + /** + * \brief Error category for verification errors + */ + inline std::error_category& signature_verification_error_category() { + class verification_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "signature_verification_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case signature_verification_error::ok: return "no error"; + case signature_verification_error::invalid_signature: return "invalid signature"; + case signature_verification_error::create_context_failed: + return "failed to verify signature: could not create context"; + case signature_verification_error::verifyinit_failed: + return "failed to verify signature: VerifyInit failed"; + case signature_verification_error::verifyupdate_failed: + return "failed to verify signature: VerifyUpdate failed"; + case signature_verification_error::verifyfinal_failed: + return "failed to verify signature: VerifyFinal failed"; + case signature_verification_error::get_key_failed: + return "failed to verify signature: Could not get key"; + default: return "unknown signature verification error"; + } + } + }; + static verification_error_cat cat; + return cat; + } + + inline std::error_code make_error_code(signature_verification_error e) { + return {static_cast(e), signature_verification_error_category()}; + } + + /** + * \brief Errors related to signature generation errors + */ + enum class signature_generation_error { + ok = 0, + hmac_failed = 10, + create_context_failed, + signinit_failed, + signupdate_failed, + signfinal_failed, + ecdsa_do_sign_failed, + digestinit_failed, + digestupdate_failed, + digestfinal_failed, + rsa_padding_failed, + rsa_private_encrypt_failed, + get_key_failed + }; + /** + * \brief Error category for signature generation errors + */ + inline std::error_category& signature_generation_error_category() { + class signature_generation_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "signature_generation_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case signature_generation_error::ok: return "no error"; + case signature_generation_error::hmac_failed: return "hmac failed"; + case signature_generation_error::create_context_failed: + return "failed to create signature: could not create context"; + case signature_generation_error::signinit_failed: + return "failed to create signature: SignInit failed"; + case signature_generation_error::signupdate_failed: + return "failed to create signature: SignUpdate failed"; + case signature_generation_error::signfinal_failed: + return "failed to create signature: SignFinal failed"; + case signature_generation_error::ecdsa_do_sign_failed: return "failed to generate ecdsa signature"; + case signature_generation_error::digestinit_failed: + return "failed to create signature: DigestInit failed"; + case signature_generation_error::digestupdate_failed: + return "failed to create signature: DigestUpdate failed"; + case signature_generation_error::digestfinal_failed: + return "failed to create signature: DigestFinal failed"; + case signature_generation_error::rsa_padding_failed: + return "failed to create signature: RSA_padding_add_PKCS1_PSS_mgf1 failed"; + case signature_generation_error::rsa_private_encrypt_failed: + return "failed to create signature: RSA_private_encrypt failed"; + case signature_generation_error::get_key_failed: + return "failed to generate signature: Could not get key"; + default: return "unknown signature generation error"; + } + } + }; + static signature_generation_error_cat cat = {}; + return cat; + } + + inline std::error_code make_error_code(signature_generation_error e) { + return {static_cast(e), signature_generation_error_category()}; + } + + /** + * \brief Errors related to token verification errors + */ + enum class token_verification_error { + ok = 0, + wrong_algorithm = 10, + missing_claim, + claim_type_missmatch, + claim_value_missmatch, + token_expired, + audience_missmatch + }; + /** + * \brief Error category for token verification errors + */ + inline std::error_category& token_verification_error_category() { + class token_verification_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "token_verification_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case token_verification_error::ok: return "no error"; + case token_verification_error::wrong_algorithm: return "wrong algorithm"; + case token_verification_error::missing_claim: return "decoded JWT is missing required claim(s)"; + case token_verification_error::claim_type_missmatch: + return "claim type does not match expected type"; + case token_verification_error::claim_value_missmatch: + return "claim value does not match expected value"; + case token_verification_error::token_expired: return "token expired"; + case token_verification_error::audience_missmatch: + return "token doesn't contain the required audience"; + default: return "unknown token verification error"; + } + } + }; + static token_verification_error_cat cat = {}; + return cat; + } + + inline std::error_code make_error_code(token_verification_error e) { + return {static_cast(e), token_verification_error_category()}; + } + + inline void throw_if_error(std::error_code ec) { + if (ec) { + if (ec.category() == rsa_error_category()) throw rsa_exception(ec); + if (ec.category() == ecdsa_error_category()) throw ecdsa_exception(ec); + if (ec.category() == signature_verification_error_category()) + throw signature_verification_exception(ec); + if (ec.category() == signature_generation_error_category()) throw signature_generation_exception(ec); + if (ec.category() == token_verification_error_category()) throw token_verification_exception(ec); + } + } + } // namespace error + + // FIXME: Remove + // Keep backward compat at least for a couple of revisions + using error::ecdsa_exception; + using error::rsa_exception; + using error::signature_generation_exception; + using error::signature_verification_exception; + using error::token_verification_exception; +} // namespace jwt +namespace std { + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; +} // namespace std +namespace jwt { + /** + * \brief A collection for working with certificates + * + * These _helpers_ are usefully when working with certificates OpenSSL APIs. + * For example, when dealing with JWKS (JSON Web Key Set)[https://tools.ietf.org/html/rfc7517] + * you maybe need to extract the modulus and exponent of an RSA Public Key. + */ + namespace helper { + /** + * \brief Extract the public key of a pem certificate + * + * \param certstr String containing the certificate encoded as pem + * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw, + std::error_code& ec) { + ec.clear(); +#if OPENSSL_VERSION_NUMBER <= 0x10100003L + std::unique_ptr certbio( + BIO_new_mem_buf(const_cast(certstr.data()), static_cast(certstr.size())), BIO_free_all); +#else + std::unique_ptr certbio( + BIO_new_mem_buf(certstr.data(), static_cast(certstr.size())), BIO_free_all); +#endif + std::unique_ptr keybio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!certbio || !keybio) { + ec = error::rsa_error::create_mem_bio_failed; + return {}; + } + + std::unique_ptr cert( + PEM_read_bio_X509(certbio.get(), nullptr, nullptr, const_cast(pw.c_str())), X509_free); + if (!cert) { + ec = error::rsa_error::cert_load_failed; + return {}; + } + std::unique_ptr key(X509_get_pubkey(cert.get()), EVP_PKEY_free); + if (!key) { + ec = error::rsa_error::get_key_failed; + return {}; + } + if (PEM_write_bio_PUBKEY(keybio.get(), key.get()) == 0) { + ec = error::rsa_error::write_key_failed; + return {}; + } + char* ptr = nullptr; + auto len = BIO_get_mem_data(keybio.get(), &ptr); + if (len <= 0 || ptr == nullptr) { + ec = error::rsa_error::convert_to_pem_failed; + return {}; + } + return {ptr, static_cast(len)}; + } + + /** + * \brief Extract the public key of a pem certificate + * + * \param certstr String containing the certificate encoded as pem + * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw = "") { + std::error_code ec; + auto res = extract_pubkey_from_cert(certstr, pw, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64 decode and return + * the results. + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param decode The function to decode the cert + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + template + std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, Decode decode, + std::error_code& ec) { + ec.clear(); + const auto decodedStr = decode(cert_base64_der_str); + auto c_str = reinterpret_cast(decodedStr.c_str()); + + std::unique_ptr cert( + d2i_X509(NULL, &c_str, static_cast(decodedStr.size())), X509_free); + std::unique_ptr certbio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!cert || !certbio) { + ec = error::rsa_error::create_mem_bio_failed; + return {}; + } + + if (!PEM_write_bio_X509(certbio.get(), cert.get())) { + ec = error::rsa_error::write_cert_failed; + return {}; + } + + char* ptr = nullptr; + const auto len = BIO_get_mem_data(certbio.get(), &ptr); + if (len <= 0 || ptr == nullptr) { + ec = error::rsa_error::convert_to_pem_failed; + return {}; + } + + return {ptr, static_cast(len)}; + } + + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64 decode and return + * the results. + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param decode The function to decode the cert + * \throw rsa_exception if an error occurred + */ + template + std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, Decode decode) { + std::error_code ec; + auto res = convert_base64_der_to_pem(cert_base64_der_str, std::move(decode), ec); + error::throw_if_error(ec); + return res; + } +#ifndef JWT_DISABLE_BASE64 + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, std::error_code& ec) { + auto decode = [](const std::string& token) { + return base::decode(base::pad(token)); + }; + return convert_base64_der_to_pem(cert_base64_der_str, std::move(decode), ec); + } + + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \throw rsa_exception if an error occurred + */ + inline std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str) { + std::error_code ec; + auto res = convert_base64_der_to_pem(cert_base64_der_str, ec); + error::throw_if_error(ec); + return res; + } +#endif + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param certstr String containing the certificate encoded as pem + * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline std::shared_ptr load_public_key_from_string(const std::string& key, + const std::string& password, std::error_code& ec) { + ec.clear(); + std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!pubkey_bio) { + ec = error::rsa_error::create_mem_bio_failed; + return nullptr; + } + if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { + auto epkey = helper::extract_pubkey_from_cert(key, password, ec); + if (ec) return nullptr; + const int len = static_cast(epkey.size()); + if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { + ec = error::rsa_error::load_key_bio_write; + return nullptr; + } + } else { + const int len = static_cast(key.size()); + if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { + ec = error::rsa_error::load_key_bio_write; + return nullptr; + } + } + + std::shared_ptr pkey( + PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, + (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` + EVP_PKEY_free); + if (!pkey) { + ec = error::rsa_error::load_key_bio_read; + return nullptr; + } + return pkey; + } + + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param certstr String containing the certificate or key encoded as pem + * \param pw Password used to decrypt certificate or key (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline std::shared_ptr load_public_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_public_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param pw Password used to decrypt key (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline std::shared_ptr + load_private_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { + std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!privkey_bio) { + ec = error::rsa_error::create_mem_bio_failed; + return nullptr; + } + const int len = static_cast(key.size()); + if (BIO_write(privkey_bio.get(), key.data(), len) != len) { + ec = error::rsa_error::load_key_bio_write; + return nullptr; + } + std::shared_ptr pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), + EVP_PKEY_free); + if (!pkey) { + ec = error::rsa_error::load_key_bio_read; + return nullptr; + } + return pkey; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param pw Password used to decrypt key (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline std::shared_ptr load_private_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_private_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * Convert a OpenSSL BIGNUM to a std::string + * \param bn BIGNUM to convert + * \return bignum as string + */ + inline +#ifdef OPENSSL10 + static std::string + bn2raw(BIGNUM* bn) +#else + static std::string + bn2raw(const BIGNUM* bn) +#endif + { + std::string res(BN_num_bytes(bn), '\0'); + BN_bn2bin(bn, (unsigned char*)res.data()); // NOLINT(google-readability-casting) requires `const_cast` + return res; + } + /** + * Convert an std::string to a OpenSSL BIGNUM + * \param raw String to convert + * \return BIGNUM representation + */ + inline static std::unique_ptr raw2bn(const std::string& raw) { + return std::unique_ptr( + BN_bin2bn(reinterpret_cast(raw.data()), static_cast(raw.size()), nullptr), + BN_free); + } + } // namespace helper + + /** + * \brief Various cryptographic algorithms when working with JWT + * + * JWT (JSON Web Tokens) signatures are typically used as the payload for a JWS (JSON Web Signature) or + * JWE (JSON Web Encryption). Both of these use various cryptographic as specified by + * [RFC7518](https://tools.ietf.org/html/rfc7518) and are exposed through the a [JOSE + * Header](https://tools.ietf.org/html/rfc7515#section-4) which points to one of the JWA (JSON Web + * Algorithms)(https://tools.ietf.org/html/rfc7518#section-3.1) + */ + namespace algorithm { + /** + * \brief "none" algorithm. + * + * Returns and empty signature and checks if the given signature is empty. + */ + struct none { + /** + * \brief Return an empty string + */ + std::string sign(const std::string& /*unused*/, std::error_code& ec) const { + ec.clear(); + return {}; + } + /** + * \brief Check if the given signature is empty. + * + * JWT's with "none" algorithm should not contain a signature. + * \param signature Signature data to verify + * \param ec error_code filled with details about the error + */ + void verify(const std::string& /*unused*/, const std::string& signature, std::error_code& ec) const { + ec.clear(); + if (!signature.empty()) { ec = error::signature_verification_error::invalid_signature; } + } + /// Get algorithm name + std::string name() const { return "none"; } + }; + /** + * \brief Base class for HMAC family of algorithms + */ + struct hmacsha { + /** + * Construct new hmac algorithm + * \param key Key to use for HMAC + * \param md Pointer to hash function + * \param name Name of the algorithm + */ + hmacsha(std::string key, const EVP_MD* (*md)(), std::string name) + : secret(std::move(key)), md(md), alg_name(std::move(name)) {} + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return HMAC signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + std::string res(static_cast(EVP_MAX_MD_SIZE), '\0'); + auto len = static_cast(res.size()); + if (HMAC(md(), secret.data(), static_cast(secret.size()), + reinterpret_cast(data.data()), static_cast(data.size()), + (unsigned char*)res.data(), // NOLINT(google-readability-casting) requires `const_cast` + &len) == nullptr) { + ec = error::signature_generation_error::hmac_failed; + return {}; + } + res.resize(len); + return res; + } + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details about failure. + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + auto res = sign(data, ec); + if (ec) return; + + bool matched = true; + for (size_t i = 0; i < std::min(res.size(), signature.size()); i++) + if (res[i] != signature[i]) matched = false; + if (res.size() != signature.size()) matched = false; + if (!matched) { + ec = error::signature_verification_error::invalid_signature; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// HMAC secrect + const std::string secret; + /// HMAC hash generator + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + }; + /** + * \brief Base class for RSA family of algorithms + */ + struct rsa { + /** + * Construct new rsa algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + * \param md Pointer to hash function + * \param name Name of the algorithm + */ + rsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, const EVP_MD* (*md)(), std::string name) + : md(md), alg_name(std::move(name)) { + if (!private_key.empty()) { + pkey = helper::load_private_key_from_string(private_key, private_key_password); + } else if (!public_key.empty()) { + pkey = helper::load_public_key_from_string(public_key, public_key_password); + } else + throw rsa_exception(error::rsa_error::no_key_provided); + } + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return RSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); +#ifdef OPENSSL10 + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); +#endif + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (!EVP_SignInit(ctx.get(), md())) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + + std::string res(EVP_PKEY_size(pkey.get()), '\0'); + unsigned int len = 0; + + if (!EVP_SignUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_generation_error::signupdate_failed; + return {}; + } + if (EVP_SignFinal(ctx.get(), (unsigned char*)res.data(), &len, pkey.get()) == 0) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + + res.resize(len); + return res; + } + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on failure + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); +#ifdef OPENSSL10 + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); +#endif + if (!ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + if (!EVP_VerifyInit(ctx.get(), md())) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } + if (!EVP_VerifyUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + auto res = EVP_VerifyFinal(ctx.get(), reinterpret_cast(signature.data()), + static_cast(signature.size()), pkey.get()); + if (res != 1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// OpenSSL structure containing converted keys + std::shared_ptr pkey; + /// Hash generator + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + }; + /** + * \brief Base class for ECDSA family of algorithms + */ + struct ecdsa { + /** + * Construct new ecdsa algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. \param public_key_password Password to decrypt public key pem. \param private_key_password Password + * to decrypt private key pem. \param md Pointer to hash function \param name Name of the algorithm + */ + ecdsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, const EVP_MD* (*md)(), std::string name, size_t siglen) + : md(md), alg_name(std::move(name)), signature_length(siglen) { + if (!public_key.empty()) { + std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!pubkey_bio) throw ecdsa_exception(error::ecdsa_error::create_mem_bio_failed); + if (public_key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { + auto epkey = helper::extract_pubkey_from_cert(public_key, public_key_password); + const int len = static_cast(epkey.size()); + if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) + throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); + } else { + const int len = static_cast(public_key.size()); + if (BIO_write(pubkey_bio.get(), public_key.data(), len) != len) + throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); + } + + pkey.reset(PEM_read_bio_EC_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)public_key_password + .c_str()), // NOLINT(google-readability-casting) requires `const_cast` + EC_KEY_free); + if (!pkey) throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); + size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); + if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) + throw ecdsa_exception(error::ecdsa_error::invalid_key_size); + } + + if (!private_key.empty()) { + std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!privkey_bio) throw ecdsa_exception(error::ecdsa_error::create_mem_bio_failed); + const int len = static_cast(private_key.size()); + if (BIO_write(privkey_bio.get(), private_key.data(), len) != len) + throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); + pkey.reset(PEM_read_bio_ECPrivateKey(privkey_bio.get(), nullptr, nullptr, + const_cast(private_key_password.c_str())), + EC_KEY_free); + if (!pkey) throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); + size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); + if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) + throw ecdsa_exception(error::ecdsa_error::invalid_key_size); + } + if (!pkey) throw ecdsa_exception(error::ecdsa_error::no_key_provided); + + if (EC_KEY_check_key(pkey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); + } + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return ECDSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + const std::string hash = generate_hash(data, ec); + if (ec) return {}; + + std::unique_ptr sig( + ECDSA_do_sign(reinterpret_cast(hash.data()), static_cast(hash.size()), + pkey.get()), + ECDSA_SIG_free); + if (!sig) { + ec = error::signature_generation_error::ecdsa_do_sign_failed; + return {}; + } +#ifdef OPENSSL10 + + auto rr = helper::bn2raw(sig->r); + auto rs = helper::bn2raw(sig->s); +#else + const BIGNUM* r; + const BIGNUM* s; + ECDSA_SIG_get0(sig.get(), &r, &s); + auto rr = helper::bn2raw(r); + auto rs = helper::bn2raw(s); +#endif + if (rr.size() > signature_length / 2 || rs.size() > signature_length / 2) + throw std::logic_error("bignum size exceeded expected length"); + rr.insert(0, signature_length / 2 - rr.size(), '\0'); + rs.insert(0, signature_length / 2 - rs.size(), '\0'); + return rr + rs; + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on error + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + const std::string hash = generate_hash(data, ec); + if (ec) return; + auto r = helper::raw2bn(signature.substr(0, signature.size() / 2)); + auto s = helper::raw2bn(signature.substr(signature.size() / 2)); + +#ifdef OPENSSL10 + ECDSA_SIG sig; + sig.r = r.get(); + sig.s = s.get(); + + if (ECDSA_do_verify((const unsigned char*)hash.data(), static_cast(hash.size()), &sig, + pkey.get()) != 1) { + ec = error::signature_verification_error::invalid_signature; + return; + } +#else + std::unique_ptr sig(ECDSA_SIG_new(), ECDSA_SIG_free); + if (!sig) { + ec = error::signature_verification_error::create_context_failed; + return; + } + + ECDSA_SIG_set0(sig.get(), r.release(), s.release()); + + if (ECDSA_do_verify(reinterpret_cast(hash.data()), static_cast(hash.size()), + sig.get(), pkey.get()) != 1) { + ec = error::signature_verification_error::invalid_signature; + return; + } +#endif + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /** + * Hash the provided data using the hash function specified in constructor + * \param data Data to hash + * \return Hash of data + */ + std::string generate_hash(const std::string& data, std::error_code& ec) const { +#ifdef OPENSSL10 + std::unique_ptr ctx(EVP_MD_CTX_create(), + &EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); +#endif + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (EVP_DigestInit(ctx.get(), md()) == 0) { + ec = error::signature_generation_error::digestinit_failed; + return {}; + } + if (EVP_DigestUpdate(ctx.get(), data.data(), data.size()) == 0) { + ec = error::signature_generation_error::digestupdate_failed; + return {}; + } + unsigned int len = 0; + std::string res(EVP_MD_CTX_size(ctx.get()), '\0'); + if (EVP_DigestFinal( + ctx.get(), + (unsigned char*)res.data(), // NOLINT(google-readability-casting) requires `const_cast` + &len) == 0) { + ec = error::signature_generation_error::digestfinal_failed; + return {}; + } + res.resize(len); + return res; + } + + /// OpenSSL struct containing keys + std::shared_ptr pkey; + /// Hash generator function + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + /// Length of the resulting signature + const size_t signature_length; + }; + +#ifndef OPENSSL110 + /** + * \brief Base class for EdDSA family of algorithms + * + * https://tools.ietf.org/html/rfc8032 + * + * The EdDSA algorithms were introduced in [OpenSSL v1.1.1](https://www.openssl.org/news/openssl-1.1.1-notes.html), + * so these algorithms are only available when building against this version or higher. + */ + struct eddsa { + /** + * Construct new eddsa algorithm + * \param public_key EdDSA public key in PEM format + * \param private_key EdDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + * \param name Name of the algorithm + */ + eddsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, std::string name) + : alg_name(std::move(name)) { + if (!private_key.empty()) { + pkey = helper::load_private_key_from_string(private_key, private_key_password); + } else if (!public_key.empty()) { + pkey = helper::load_public_key_from_string(public_key, public_key_password); + } else + throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); + } + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return EdDSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (!EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get())) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + + size_t len = EVP_PKEY_size(pkey.get()); + std::string res(len, '\0'); + +// LibreSSL is the special kid in the block, as it does not support EVP_DigestSign. +// OpenSSL on the otherhand does not support using EVP_DigestSignUpdate for eddsa, which is why we end up with this +// mess. +#ifdef LIBRESSL_VERSION_NUMBER + ERR_clear_error(); + if (EVP_DigestSignUpdate(ctx.get(), reinterpret_cast(data.data()), data.size()) != + 1) { + std::cout << ERR_error_string(ERR_get_error(), NULL) << std::endl; + ec = error::signature_generation_error::signupdate_failed; + return {}; + } + if (EVP_DigestSignFinal(ctx.get(), reinterpret_cast(&res[0]), &len) != 1) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } +#else + if (EVP_DigestSign(ctx.get(), reinterpret_cast(&res[0]), &len, + reinterpret_cast(data.data()), data.size()) != 1) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } +#endif + + res.resize(len); + return res; + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on error + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); + if (!ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + if (!EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get())) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } +// LibreSSL is the special kid in the block, as it does not support EVP_DigestVerify. +// OpenSSL on the otherhand does not support using EVP_DigestVerifyUpdate for eddsa, which is why we end up with this +// mess. +#ifdef LIBRESSL_VERSION_NUMBER + if (EVP_DigestVerifyUpdate(ctx.get(), reinterpret_cast(data.data()), + data.size()) != 1) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + if (EVP_DigestVerifyFinal(ctx.get(), reinterpret_cast(signature.data()), + signature.size()) != 1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } +#else + auto res = EVP_DigestVerify(ctx.get(), reinterpret_cast(signature.data()), + signature.size(), reinterpret_cast(data.data()), + data.size()); + if (res != 1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } +#endif + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// OpenSSL struct containing keys + std::shared_ptr pkey; + /// algorithm's name + const std::string alg_name; + }; +#endif + /** + * \brief Base class for PSS-RSA family of algorithms + */ + struct pss { + /** + * Construct new pss algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + * \param md Pointer to hash function + * \param name Name of the algorithm + */ + pss(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, const EVP_MD* (*md)(), std::string name) + : md(md), alg_name(std::move(name)) { + if (!private_key.empty()) { + pkey = helper::load_private_key_from_string(private_key, private_key_password); + } else if (!public_key.empty()) { + pkey = helper::load_public_key_from_string(public_key, public_key_password); + } else + throw rsa_exception(error::rsa_error::no_key_provided); + } + + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return ECDSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + auto hash = this->generate_hash(data, ec); + if (ec) return {}; + + std::unique_ptr key(EVP_PKEY_get1_RSA(pkey.get()), RSA_free); + if (!key) { + ec = error::signature_generation_error::get_key_failed; + return {}; + } + const int size = RSA_size(key.get()); + + std::string padded(size, 0x00); + if (RSA_padding_add_PKCS1_PSS_mgf1( + key.get(), (unsigned char*)padded.data(), reinterpret_cast(hash.data()), + md(), md(), -1) == 0) { // NOLINT(google-readability-casting) requires `const_cast` + ec = error::signature_generation_error::rsa_padding_failed; + return {}; + } + + std::string res(size, 0x00); + if (RSA_private_encrypt(size, reinterpret_cast(padded.data()), + (unsigned char*)res.data(), key.get(), RSA_NO_PADDING) < + 0) { // NOLINT(google-readability-casting) requires `const_cast` + ec = error::signature_generation_error::rsa_private_encrypt_failed; + return {}; + } + return res; + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with error details + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + auto hash = this->generate_hash(data, ec); + if (ec) return; + + std::unique_ptr key(EVP_PKEY_get1_RSA(pkey.get()), RSA_free); + if (!key) { + ec = error::signature_verification_error::get_key_failed; + return; + } + const int size = RSA_size(key.get()); + + std::string sig(size, 0x00); + if (RSA_public_decrypt( + static_cast(signature.size()), reinterpret_cast(signature.data()), + (unsigned char*)sig.data(), // NOLINT(google-readability-casting) requires `const_cast` + key.get(), RSA_NO_PADDING) == 0) { + ec = error::signature_verification_error::invalid_signature; + return; + } + + if (RSA_verify_PKCS1_PSS_mgf1(key.get(), reinterpret_cast(hash.data()), md(), + md(), reinterpret_cast(sig.data()), -1) == 0) { + ec = error::signature_verification_error::invalid_signature; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /** + * Hash the provided data using the hash function specified in constructor + * \param data Data to hash + * \return Hash of data + */ + std::string generate_hash(const std::string& data, std::error_code& ec) const { +#ifdef OPENSSL10 + std::unique_ptr ctx(EVP_MD_CTX_create(), + &EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); +#endif + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (EVP_DigestInit(ctx.get(), md()) == 0) { + ec = error::signature_generation_error::digestinit_failed; + return {}; + } + if (EVP_DigestUpdate(ctx.get(), data.data(), data.size()) == 0) { + ec = error::signature_generation_error::digestupdate_failed; + return {}; + } + unsigned int len = 0; + std::string res(EVP_MD_CTX_size(ctx.get()), '\0'); + if (EVP_DigestFinal(ctx.get(), (unsigned char*)res.data(), &len) == + 0) { // NOLINT(google-readability-casting) requires `const_cast` + ec = error::signature_generation_error::digestfinal_failed; + return {}; + } + res.resize(len); + return res; + } + + /// OpenSSL structure containing keys + std::shared_ptr pkey; + /// Hash generator function + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + }; + + /** + * HS256 algorithm + */ + struct hs256 : public hmacsha { + /** + * Construct new instance of algorithm + * \param key HMAC signing key + */ + explicit hs256(std::string key) : hmacsha(std::move(key), EVP_sha256, "HS256") {} + }; + /** + * HS384 algorithm + */ + struct hs384 : public hmacsha { + /** + * Construct new instance of algorithm + * \param key HMAC signing key + */ + explicit hs384(std::string key) : hmacsha(std::move(key), EVP_sha384, "HS384") {} + }; + /** + * HS512 algorithm + */ + struct hs512 : public hmacsha { + /** + * Construct new instance of algorithm + * \param key HMAC signing key + */ + explicit hs512(std::string key) : hmacsha(std::move(key), EVP_sha512, "HS512") {} + }; + /** + * RS256 algorithm + */ + struct rs256 : public rsa { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit rs256(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : rsa(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "RS256") {} + }; + /** + * RS384 algorithm + */ + struct rs384 : public rsa { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit rs384(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : rsa(public_key, private_key, public_key_password, private_key_password, EVP_sha384, "RS384") {} + }; + /** + * RS512 algorithm + */ + struct rs512 : public rsa { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit rs512(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : rsa(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "RS512") {} + }; + /** + * ES256 algorithm + */ + struct es256 : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit es256(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "ES256", 64) {} + }; + /** + * ES384 algorithm + */ + struct es384 : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit es384(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha384, "ES384", 96) {} + }; + /** + * ES512 algorithm + */ + struct es512 : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit es512(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "ES512", 132) {} + }; + +#ifndef OPENSSL110 + /** + * Ed25519 algorithm + * + * https://en.wikipedia.org/wiki/EdDSA#Ed25519 + * + * Requires at least OpenSSL 1.1.1. + */ + struct ed25519 : public eddsa { + /** + * Construct new instance of algorithm + * \param public_key Ed25519 public key in PEM format + * \param private_key Ed25519 private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit ed25519(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : eddsa(public_key, private_key, public_key_password, private_key_password, "EdDSA") {} + }; + + /** + * Ed448 algorithm + * + * https://en.wikipedia.org/wiki/EdDSA#Ed448 + * + * Requires at least OpenSSL 1.1.1. + */ + struct ed448 : public eddsa { + /** + * Construct new instance of algorithm + * \param public_key Ed448 public key in PEM format + * \param private_key Ed448 private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit ed448(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : eddsa(public_key, private_key, public_key_password, private_key_password, "EdDSA") {} + }; +#endif + + /** + * PS256 algorithm + */ + struct ps256 : public pss { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit ps256(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : pss(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "PS256") {} + }; + /** + * PS384 algorithm + */ + struct ps384 : public pss { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit ps384(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : pss(public_key, private_key, public_key_password, private_key_password, EVP_sha384, "PS384") {} + }; + /** + * PS512 algorithm + */ + struct ps512 : public pss { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit ps512(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : pss(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "PS512") {} + }; + } // namespace algorithm + + /** + * \brief JSON Abstractions for working with any library + */ + namespace json { + /** + * \brief Generic JSON types used in JWTs + * + * This enum is to abstract the third party underlying types + */ + enum class type { boolean, integer, number, string, array, object }; + } // namespace json + + namespace details { +#ifdef __cpp_lib_void_t + template + using void_t = std::void_t; +#else + // https://en.cppreference.com/w/cpp/types/void_t + template + struct make_void { + using type = void; + }; + + template + using void_t = typename make_void::type; +#endif + +#ifdef __cpp_lib_experimental_detect + template class _Op, typename... _Args> + using is_detected = std::experimental::is_detected<_Op, _Args...>; + + template class _Op, typename... _Args> + using is_detected_t = std::experimental::detected_t<_Op, _Args...>; +#else + struct nonesuch { + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; + }; + + // https://en.cppreference.com/w/cpp/experimental/is_detected + template class Op, class... Args> + struct detector { + using value = std::false_type; + using type = Default; + }; + + template class Op, class... Args> + struct detector>, Op, Args...> { + using value = std::true_type; + using type = Op; + }; + + template class Op, class... Args> + using is_detected = typename detector::value; + + template class Op, class... Args> + using is_detected_t = typename detector::type; +#endif + + template + using get_type_function = decltype(traits_type::get_type); + + template + using is_get_type_signature = + typename std::is_same, json::type(const value_type&)>; + + template + struct supports_get_type { + static constexpr auto value = is_detected::value && + std::is_function>::value && + is_get_type_signature::value; + }; + + template + using as_object_function = decltype(traits_type::as_object); + + template + using is_as_object_signature = + typename std::is_same, object_type(const value_type&)>; + + template + struct supports_as_object { + static constexpr auto value = std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_object_signature::value; + }; + + template + using as_array_function = decltype(traits_type::as_array); + + template + using is_as_array_signature = + typename std::is_same, array_type(const value_type&)>; + + template + struct supports_as_array { + static constexpr auto value = std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_array_signature::value; + }; + + template + using as_string_function = decltype(traits_type::as_string); + + template + using is_as_string_signature = + typename std::is_same, string_type(const value_type&)>; + + template + struct supports_as_string { + static constexpr auto value = std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_string_signature::value; + }; + + template + using as_number_function = decltype(traits_type::as_number); + + template + using is_as_number_signature = + typename std::is_same, number_type(const value_type&)>; + + template + struct supports_as_number { + static constexpr auto value = std::is_floating_point::value && + std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_number_signature::value; + }; + + template + using as_integer_function = decltype(traits_type::as_int); + + template + using is_as_integer_signature = + typename std::is_same, integer_type(const value_type&)>; + + template + struct supports_as_integer { + static constexpr auto value = std::is_signed::value && + !std::is_floating_point::value && + std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_integer_signature::value; + }; + + template + using as_boolean_function = decltype(traits_type::as_bool); + + template + using is_as_boolean_signature = + typename std::is_same, boolean_type(const value_type&)>; + + template + struct supports_as_boolean { + static constexpr auto value = std::is_convertible::value && + std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_boolean_signature::value; + }; + + template + struct is_valid_traits { + // Internal assertions for better feedback + static_assert(supports_get_type::value, + "traits must provide `jwt::json::type get_type(const value_type&)`"); + static_assert(supports_as_object::value, + "traits must provide `object_type as_object(const value_type&)`"); + static_assert(supports_as_array::value, + "traits must provide `array_type as_array(const value_type&)`"); + static_assert(supports_as_string::value, + "traits must provide `string_type as_string(const value_type&)`"); + static_assert(supports_as_number::value, + "traits must provide `number_type as_number(const value_type&)`"); + static_assert( + supports_as_integer::value, + "traits must provide `integer_type as_int(const value_type&)`"); + static_assert( + supports_as_boolean::value, + "traits must provide `boolean_type as_bool(const value_type&)`"); + + static constexpr auto value = + supports_get_type::value && + supports_as_object::value && + supports_as_array::value && + supports_as_string::value && + supports_as_number::value && + supports_as_integer::value && + supports_as_boolean::value; + }; + + template + struct is_valid_json_value { + static constexpr auto value = + std::is_default_constructible::value && + std::is_constructible::value && // a more generic is_copy_constructible + std::is_move_constructible::value && std::is_assignable::value && + std::is_copy_assignable::value && std::is_move_assignable::value; + // TODO(cmcarthur): Stream operators + }; + + template + using has_mapped_type = typename traits_type::mapped_type; + + template + using has_key_type = typename traits_type::key_type; + + template + using has_value_type = typename traits_type::value_type; + + template + using has_iterator = typename object_type::iterator; + + template + using has_const_iterator = typename object_type::const_iterator; + + template + using is_begin_signature = + typename std::is_same().begin()), has_iterator>; + + template + using is_begin_const_signature = + typename std::is_same().begin()), has_const_iterator>; + + template + struct supports_begin { + static constexpr auto value = + is_detected::value && is_detected::value && + is_begin_signature::value && is_begin_const_signature::value; + }; + + template + using is_end_signature = + typename std::is_same().end()), has_iterator>; + + template + using is_end_const_signature = + typename std::is_same().end()), has_const_iterator>; + + template + struct supports_end { + static constexpr auto value = + is_detected::value && is_detected::value && + is_end_signature::value && is_end_const_signature::value; + }; + + template + using is_count_signature = typename std::is_integral().count(std::declval()))>; + + template + using is_subcription_operator_signature = + typename std::is_same()[std::declval()]), + value_type&>; + + template + using is_at_const_signature = + typename std::is_same().at(std::declval())), + const value_type&>; + + template + struct is_valid_json_object { + static constexpr auto value = + is_detected::value && + std::is_same::value && + is_detected::value && + std::is_same::value && + supports_begin::value && supports_end::value && + is_count_signature::value && + is_subcription_operator_signature::value && + is_at_const_signature::value; + + static constexpr auto supports_claims_transform = + value && is_detected::value && + std::is_same>::value; + }; + + template + struct is_valid_json_array { + static constexpr auto value = std::is_same::value; + }; + + template + struct is_valid_json_types { + // Internal assertions for better feedback + static_assert(is_valid_json_value::value, + "value type must meet basic requirements, default constructor, copyable, moveable"); + static_assert(is_valid_json_object::value, + "object_type must be a string_type to value_type container"); + static_assert(is_valid_json_array::value, + "array_type must be a container of value_type"); + + static constexpr auto value = is_valid_json_object::value && + is_valid_json_value::value && + is_valid_json_array::value; + }; + } // namespace details + + /** + * \brief a class to store a generic JSON value as claim + * + * The default template parameters use [picojson](https://github.com/kazuho/picojson) + * + * \tparam json_traits : JSON implementation traits + * + * \see [RFC 7519: JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) + */ + template + class basic_claim { + /** + * The reason behind this is to provide an expressive abstraction without + * over complexifying the API. For more information take the time to read + * https://github.com/nlohmann/json/issues/774. It maybe be expanded to + * support custom string types. + */ + static_assert(std::is_same::value, + "string_type must be a std::string."); + + static_assert( + details::is_valid_json_types::value, + "must staisfy json container requirements"); + static_assert(details::is_valid_traits::value, "traits must satisfy requirements"); + + typename json_traits::value_type val; + + public: + using set_t = std::set; + + basic_claim() = default; + basic_claim(const basic_claim&) = default; + basic_claim(basic_claim&&) = default; + basic_claim& operator=(const basic_claim&) = default; + basic_claim& operator=(basic_claim&&) = default; + ~basic_claim() = default; + + JWT_CLAIM_EXPLICIT basic_claim(typename json_traits::string_type s) : val(std::move(s)) {} + JWT_CLAIM_EXPLICIT basic_claim(const date& d) + : val(typename json_traits::integer_type(std::chrono::system_clock::to_time_t(d))) {} + JWT_CLAIM_EXPLICIT basic_claim(typename json_traits::array_type a) : val(std::move(a)) {} + JWT_CLAIM_EXPLICIT basic_claim(typename json_traits::value_type v) : val(std::move(v)) {} + JWT_CLAIM_EXPLICIT basic_claim(const set_t& s) : val(typename json_traits::array_type(s.begin(), s.end())) {} + template + basic_claim(Iterator begin, Iterator end) : val(typename json_traits::array_type(begin, end)) {} + + /** + * Get wrapped JSON value + * \return Wrapped JSON value + */ + typename json_traits::value_type to_json() const { return val; } + + /** + * Parse input stream into underlying JSON value + * \return input stream + */ + std::istream& operator>>(std::istream& is) { return is >> val; } + + /** + * Serialize claim to output stream from wrapped JSON value + * \return ouput stream + */ + std::ostream& operator<<(std::ostream& os) { return os << val; } + + /** + * Get type of contained JSON value + * \return Type + * \throw std::logic_error An internal error occured + */ + json::type get_type() const { return json_traits::get_type(val); } + + /** + * Get the contained JSON value as a string + * \return content as string + * \throw std::bad_cast Content was not a string + */ + typename json_traits::string_type as_string() const { return json_traits::as_string(val); } + + /** + * Get the contained JSON value as a date + * \return content as date + * \throw std::bad_cast Content was not a date + */ + date as_date() const { return std::chrono::system_clock::from_time_t(as_int()); } + + /** + * Get the contained JSON value as an array + * \return content as array + * \throw std::bad_cast Content was not an array + */ + typename json_traits::array_type as_array() const { return json_traits::as_array(val); } + + /** + * Get the contained JSON value as a set of strings + * \return content as set of strings + * \throw std::bad_cast Content was not an array of string + */ + set_t as_set() const { + set_t res; + for (const auto& e : json_traits::as_array(val)) { + res.insert(json_traits::as_string(e)); + } + return res; + } + + /** + * Get the contained JSON value as an integer + * \return content as int + * \throw std::bad_cast Content was not an int + */ + typename json_traits::integer_type as_int() const { return json_traits::as_int(val); } + + /** + * Get the contained JSON value as a bool + * \return content as bool + * \throw std::bad_cast Content was not a bool + */ + typename json_traits::boolean_type as_bool() const { return json_traits::as_bool(val); } + + /** + * Get the contained JSON value as a number + * \return content as double + * \throw std::bad_cast Content was not a number + */ + typename json_traits::number_type as_number() const { return json_traits::as_number(val); } + }; + + namespace error { + struct invalid_json_exception : public std::runtime_error { + invalid_json_exception() : runtime_error("invalid json") {} + }; + struct claim_not_present_exception : public std::out_of_range { + claim_not_present_exception() : out_of_range("claim not found") {} + }; + } // namespace error + + namespace details { + template + class map_of_claims { + typename json_traits::object_type claims; + + public: + using basic_claim_t = basic_claim; + using iterator = typename json_traits::object_type::iterator; + using const_iterator = typename json_traits::object_type::const_iterator; + + map_of_claims() = default; + map_of_claims(const map_of_claims&) = default; + map_of_claims(map_of_claims&&) = default; + map_of_claims& operator=(const map_of_claims&) = default; + map_of_claims& operator=(map_of_claims&&) = default; + + map_of_claims(typename json_traits::object_type json) : claims(std::move(json)) {} + + iterator begin() { return claims.begin(); } + iterator end() { return claims.end(); } + const_iterator cbegin() const { return claims.begin(); } + const_iterator cend() const { return claims.end(); } + const_iterator begin() const { return claims.begin(); } + const_iterator end() const { return claims.end(); } + + /** + * \brief Parse a JSON string into a map of claims + * + * The implication is that a "map of claims" is identic to a JSON object + * + * \param str JSON data to be parse as an object + * \return content as JSON object + */ + static typename json_traits::object_type parse_claims(const typename json_traits::string_type& str) { + typename json_traits::value_type val; + if (!json_traits::parse(val, str)) throw error::invalid_json_exception(); + + return json_traits::as_object(val); + }; + + /** + * Check if a claim is present in the map + * \return true if claim was present, false otherwise + */ + bool has_claim(const typename json_traits::string_type& name) const noexcept { + return claims.count(name) != 0; + } + + /** + * Get a claim by name + * + * \param name the name of the desired claim + * \return Requested claim + * \throw jwt::error::claim_not_present_exception if the claim was not present + */ + basic_claim_t get_claim(const typename json_traits::string_type& name) const { + if (!has_claim(name)) throw error::claim_not_present_exception(); + return basic_claim_t{claims.at(name)}; + } + + std::unordered_map get_claims() const { + static_assert( + details::is_valid_json_object::supports_claims_transform, + "currently there is a limitation on the internal implemantation of the `object_type` to have an " + "`std::pair` like `value_type`"); + + std::unordered_map res; + std::transform(claims.begin(), claims.end(), std::inserter(res, res.end()), + [](const typename json_traits::object_type::value_type& val) { + return std::make_pair(val.first, basic_claim_t{val.second}); + }); + return res; + } + }; + } // namespace details + + /** + * Base class that represents a token payload. + * Contains Convenience accessors for common claims. + */ + template + class payload { + protected: + details::map_of_claims payload_claims; + + public: + using basic_claim_t = basic_claim; + + /** + * Check if issuer is present ("iss") + * \return true if present, false otherwise + */ + bool has_issuer() const noexcept { return has_payload_claim("iss"); } + /** + * Check if subject is present ("sub") + * \return true if present, false otherwise + */ + bool has_subject() const noexcept { return has_payload_claim("sub"); } + /** + * Check if audience is present ("aud") + * \return true if present, false otherwise + */ + bool has_audience() const noexcept { return has_payload_claim("aud"); } + /** + * Check if expires is present ("exp") + * \return true if present, false otherwise + */ + bool has_expires_at() const noexcept { return has_payload_claim("exp"); } + /** + * Check if not before is present ("nbf") + * \return true if present, false otherwise + */ + bool has_not_before() const noexcept { return has_payload_claim("nbf"); } + /** + * Check if issued at is present ("iat") + * \return true if present, false otherwise + */ + bool has_issued_at() const noexcept { return has_payload_claim("iat"); } + /** + * Check if token id is present ("jti") + * \return true if present, false otherwise + */ + bool has_id() const noexcept { return has_payload_claim("jti"); } + /** + * Get issuer claim + * \return issuer as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_issuer() const { return get_payload_claim("iss").as_string(); } + /** + * Get subject claim + * \return subject as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_subject() const { return get_payload_claim("sub").as_string(); } + /** + * Get audience claim + * \return audience as a set of strings + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a set (Should not happen in a valid token) + */ + typename basic_claim_t::set_t get_audience() const { + auto aud = get_payload_claim("aud"); + if (aud.get_type() == json::type::string) return {aud.as_string()}; + + return aud.as_set(); + } + /** + * Get expires claim + * \return expires as a date in utc + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a date (Should not happen in a valid token) + */ + date get_expires_at() const { return get_payload_claim("exp").as_date(); } + /** + * Get not valid before claim + * \return nbf date in utc + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a date (Should not happen in a valid token) + */ + date get_not_before() const { return get_payload_claim("nbf").as_date(); } + /** + * Get issued at claim + * \return issued at as date in utc + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a date (Should not happen in a valid token) + */ + date get_issued_at() const { return get_payload_claim("iat").as_date(); } + /** + * Get id claim + * \return id as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_id() const { return get_payload_claim("jti").as_string(); } + /** + * Check if a payload claim is present + * \return true if claim was present, false otherwise + */ + bool has_payload_claim(const typename json_traits::string_type& name) const noexcept { + return payload_claims.has_claim(name); + } + /** + * Get payload claim + * \return Requested claim + * \throw std::runtime_error If claim was not present + */ + basic_claim_t get_payload_claim(const typename json_traits::string_type& name) const { + return payload_claims.get_claim(name); + } + }; + + /** + * Base class that represents a token header. + * Contains Convenience accessors for common claims. + */ + template + class header { + protected: + details::map_of_claims header_claims; + + public: + using basic_claim_t = basic_claim; + /** + * Check if algortihm is present ("alg") + * \return true if present, false otherwise + */ + bool has_algorithm() const noexcept { return has_header_claim("alg"); } + /** + * Check if type is present ("typ") + * \return true if present, false otherwise + */ + bool has_type() const noexcept { return has_header_claim("typ"); } + /** + * Check if content type is present ("cty") + * \return true if present, false otherwise + */ + bool has_content_type() const noexcept { return has_header_claim("cty"); } + /** + * Check if key id is present ("kid") + * \return true if present, false otherwise + */ + bool has_key_id() const noexcept { return has_header_claim("kid"); } + /** + * Get algorithm claim + * \return algorithm as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_algorithm() const { return get_header_claim("alg").as_string(); } + /** + * Get type claim + * \return type as a string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_type() const { return get_header_claim("typ").as_string(); } + /** + * Get content type claim + * \return content type as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_content_type() const { return get_header_claim("cty").as_string(); } + /** + * Get key id claim + * \return key id as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_key_id() const { return get_header_claim("kid").as_string(); } + /** + * Check if a header claim is present + * \return true if claim was present, false otherwise + */ + bool has_header_claim(const typename json_traits::string_type& name) const noexcept { + return header_claims.has_claim(name); + } + /** + * Get header claim + * \return Requested claim + * \throw std::runtime_error If claim was not present + */ + basic_claim_t get_header_claim(const typename json_traits::string_type& name) const { + return header_claims.get_claim(name); + } + }; + + /** + * Class containing all information about a decoded token + */ + template + class decoded_jwt : public header, public payload { + protected: + /// Unmodifed token, as passed to constructor + const typename json_traits::string_type token; + /// Header part decoded from base64 + typename json_traits::string_type header; + /// Unmodified header part in base64 + typename json_traits::string_type header_base64; + /// Payload part decoded from base64 + typename json_traits::string_type payload; + /// Unmodified payload part in base64 + typename json_traits::string_type payload_base64; + /// Signature part decoded from base64 + typename json_traits::string_type signature; + /// Unmodified signature part in base64 + typename json_traits::string_type signature_base64; + + public: + using basic_claim_t = basic_claim; +#ifndef JWT_DISABLE_BASE64 + /** + * \brief Parses a given token + * + * \note Decodes using the jwt::base64url which supports an std::string + * + * \param token The token to parse + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + JWT_CLAIM_EXPLICIT decoded_jwt(const typename json_traits::string_type& token) + : decoded_jwt(token, [](const typename json_traits::string_type& token) { + return base::decode(base::pad(token)); + }) {} +#endif + /** + * \brief Parses a given token + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64url decode and + * return the results. + * \param token The token to parse + * \param decode The function to decode the token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt(const typename json_traits::string_type& token, Decode decode) : token(token) { + auto hdr_end = token.find('.'); + if (hdr_end == json_traits::string_type::npos) throw std::invalid_argument("invalid token supplied"); + auto payload_end = token.find('.', hdr_end + 1); + if (payload_end == json_traits::string_type::npos) throw std::invalid_argument("invalid token supplied"); + header_base64 = token.substr(0, hdr_end); + payload_base64 = token.substr(hdr_end + 1, payload_end - hdr_end - 1); + signature_base64 = token.substr(payload_end + 1); + + header = decode(header_base64); + payload = decode(payload_base64); + signature = decode(signature_base64); + + this->header_claims = details::map_of_claims::parse_claims(header); + this->payload_claims = details::map_of_claims::parse_claims(payload); + } + + /** + * Get token string, as passed to constructor + * \return token as passed to constructor + */ + const typename json_traits::string_type& get_token() const noexcept { return token; } + /** + * Get header part as json string + * \return header part after base64 decoding + */ + const typename json_traits::string_type& get_header() const noexcept { return header; } + /** + * Get payload part as json string + * \return payload part after base64 decoding + */ + const typename json_traits::string_type& get_payload() const noexcept { return payload; } + /** + * Get signature part as json string + * \return signature part after base64 decoding + */ + const typename json_traits::string_type& get_signature() const noexcept { return signature; } + /** + * Get header part as base64 string + * \return header part before base64 decoding + */ + const typename json_traits::string_type& get_header_base64() const noexcept { return header_base64; } + /** + * Get payload part as base64 string + * \return payload part before base64 decoding + */ + const typename json_traits::string_type& get_payload_base64() const noexcept { return payload_base64; } + /** + * Get signature part as base64 string + * \return signature part before base64 decoding + */ + const typename json_traits::string_type& get_signature_base64() const noexcept { return signature_base64; } + /** + * Get all payload claims + * \return map of claims + */ + std::unordered_map get_payload_claims() const { + return this->payload_claims.get_claims(); + } + /** + * Get all header claims + * \return map of claims + */ + std::unordered_map get_header_claims() const { + return this->header_claims.get_claims(); + } + }; + + /** + * Builder class to build and sign a new token + * Use jwt::create() to get an instance of this class. + */ + template + class builder { + typename json_traits::object_type header_claims; + typename json_traits::object_type payload_claims; + + public: + builder() = default; + /** + * Set a header claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_header_claim(const typename json_traits::string_type& id, typename json_traits::value_type c) { + header_claims[id] = std::move(c); + return *this; + } + + /** + * Set a header claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_header_claim(const typename json_traits::string_type& id, basic_claim c) { + header_claims[id] = c.to_json(); + return *this; + } + /** + * Set a payload claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_payload_claim(const typename json_traits::string_type& id, typename json_traits::value_type c) { + payload_claims[id] = std::move(c); + return *this; + } + /** + * Set a payload claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_payload_claim(const typename json_traits::string_type& id, basic_claim c) { + payload_claims[id] = c.to_json(); + return *this; + } + /** + * Set algorithm claim + * You normally don't need to do this, as the algorithm is automatically set if you don't change it. + * \param str Name of algorithm + * \return *this to allow for method chaining + */ + builder& set_algorithm(typename json_traits::string_type str) { + return set_header_claim("alg", typename json_traits::value_type(str)); + } + /** + * Set type claim + * \param str Type to set + * \return *this to allow for method chaining + */ + builder& set_type(typename json_traits::string_type str) { + return set_header_claim("typ", typename json_traits::value_type(str)); + } + /** + * Set content type claim + * \param str Type to set + * \return *this to allow for method chaining + */ + builder& set_content_type(typename json_traits::string_type str) { + return set_header_claim("cty", typename json_traits::value_type(str)); + } + /** + * Set key id claim + * \param str Key id to set + * \return *this to allow for method chaining + */ + builder& set_key_id(typename json_traits::string_type str) { + return set_header_claim("kid", typename json_traits::value_type(str)); + } + /** + * Set issuer claim + * \param str Issuer to set + * \return *this to allow for method chaining + */ + builder& set_issuer(typename json_traits::string_type str) { + return set_payload_claim("iss", typename json_traits::value_type(str)); + } + /** + * Set subject claim + * \param str Subject to set + * \return *this to allow for method chaining + */ + builder& set_subject(typename json_traits::string_type str) { + return set_payload_claim("sub", typename json_traits::value_type(str)); + } + /** + * Set audience claim + * \param a Audience set + * \return *this to allow for method chaining + */ + builder& set_audience(typename json_traits::array_type a) { + return set_payload_claim("aud", typename json_traits::value_type(a)); + } + /** + * Set audience claim + * \param aud Single audience + * \return *this to allow for method chaining + */ + builder& set_audience(typename json_traits::string_type aud) { + return set_payload_claim("aud", typename json_traits::value_type(aud)); + } + /** + * Set expires at claim + * \param d Expires time + * \return *this to allow for method chaining + */ + builder& set_expires_at(const date& d) { return set_payload_claim("exp", basic_claim(d)); } + /** + * Set not before claim + * \param d First valid time + * \return *this to allow for method chaining + */ + builder& set_not_before(const date& d) { return set_payload_claim("nbf", basic_claim(d)); } + /** + * Set issued at claim + * \param d Issued at time, should be current time + * \return *this to allow for method chaining + */ + builder& set_issued_at(const date& d) { return set_payload_claim("iat", basic_claim(d)); } + /** + * Set id claim + * \param str ID to set + * \return *this to allow for method chaining + */ + builder& set_id(const typename json_traits::string_type& str) { + return set_payload_claim("jti", typename json_traits::value_type(str)); + } + + /** + * Sign token and return result + * \tparam Algo Callable method which takes a string_type and return the signed input as a string_type + * \tparam Encode Callable method which takes a string_type and base64url safe encodes it, + * MUST return the result with no padding; trim the result. + * \param algo Instance of an algorithm to sign the token with + * \param encode Callable to transform the serialized json to base64 with no padding + * \return Final token as a string + * + * \note If the 'alg' header in not set in the token it will be set to `algo.name()` + */ + template + typename json_traits::string_type sign(const Algo& algo, Encode encode) const { + std::error_code ec; + auto res = sign(algo, encode, ec); + error::throw_if_error(ec); + return res; + } +#ifndef JWT_DISABLE_BASE64 + /** + * Sign token and return result + * + * using the `jwt::base` functions provided + * + * \param algo Instance of an algorithm to sign the token with + * \return Final token as a string + */ + template + typename json_traits::string_type sign(const Algo& algo) const { + std::error_code ec; + auto res = sign(algo, ec); + error::throw_if_error(ec); + return res; + } +#endif + + /** + * Sign token and return result + * \tparam Algo Callable method which takes a string_type and return the signed input as a string_type + * \tparam Encode Callable method which takes a string_type and base64url safe encodes it, + * MUST return the result with no padding; trim the result. + * \param algo Instance of an algorithm to sign the token with + * \param encode Callable to transform the serialized json to base64 with no padding + * \param ec error_code filled with details on error + * \return Final token as a string + * + * \note If the 'alg' header in not set in the token it will be set to `algo.name()` + */ + template + typename json_traits::string_type sign(const Algo& algo, Encode encode, std::error_code& ec) const { + // make a copy such that a builder can be re-used + typename json_traits::object_type obj_header = header_claims; + if (header_claims.count("alg") == 0) obj_header["alg"] = typename json_traits::value_type(algo.name()); + + const auto header = encode(json_traits::serialize(typename json_traits::value_type(obj_header))); + const auto payload = encode(json_traits::serialize(typename json_traits::value_type(payload_claims))); + const auto token = header + "." + payload; + + auto signature = algo.sign(token, ec); + if (ec) return {}; + + return token + "." + encode(signature); + } +#ifndef JWT_DISABLE_BASE64 + /** + * Sign token and return result + * + * using the `jwt::base` functions provided + * + * \param algo Instance of an algorithm to sign the token with + * \param ec error_code filled with details on error + * \return Final token as a string + */ + template + typename json_traits::string_type sign(const Algo& algo, std::error_code& ec) const { + return sign( + algo, + [](const typename json_traits::string_type& data) { + return base::trim(base::encode(data)); + }, + ec); + } +#endif + }; + + /** + * Verifier class used to check if a decoded token contains all claims required by your application and has a valid + * signature. + */ + template + class verifier { + struct algo_base { + virtual ~algo_base() = default; + virtual void verify(const std::string& data, const std::string& sig, std::error_code& ec) = 0; + }; + template + struct algo : public algo_base { + T alg; + explicit algo(T a) : alg(a) {} + void verify(const std::string& data, const std::string& sig, std::error_code& ec) override { + alg.verify(data, sig, ec); + } + }; + + using basic_claim_t = basic_claim; + /// Required claims + std::unordered_map claims; + /// Leeway time for exp, nbf and iat + size_t default_leeway = 0; + /// Instance of clock type + Clock clock; + /// Supported algorithms + std::unordered_map> algs; + + public: + /** + * Constructor for building a new verifier instance + * \param c Clock instance + */ + explicit verifier(Clock c) : clock(c) {} + + /** + * Set default leeway to use. + * \param leeway Default leeway to use if not specified otherwise + * \return *this to allow chaining + */ + verifier& leeway(size_t leeway) { + default_leeway = leeway; + return *this; + } + /** + * Set leeway for expires at. + * If not specified the default leeway will be used. + * \param leeway Set leeway to use for expires at. + * \return *this to allow chaining + */ + verifier& expires_at_leeway(size_t leeway) { + return with_claim("exp", basic_claim_t(std::chrono::system_clock::from_time_t(leeway))); + } + /** + * Set leeway for not before. + * If not specified the default leeway will be used. + * \param leeway Set leeway to use for not before. + * \return *this to allow chaining + */ + verifier& not_before_leeway(size_t leeway) { + return with_claim("nbf", basic_claim_t(std::chrono::system_clock::from_time_t(leeway))); + } + /** + * Set leeway for issued at. + * If not specified the default leeway will be used. + * \param leeway Set leeway to use for issued at. + * \return *this to allow chaining + */ + verifier& issued_at_leeway(size_t leeway) { + return with_claim("iat", basic_claim_t(std::chrono::system_clock::from_time_t(leeway))); + } + /** + * Set an issuer to check for. + * Check is casesensitive. + * \param iss Issuer to check for. + * \return *this to allow chaining + */ + verifier& with_issuer(const typename json_traits::string_type& iss) { + return with_claim("iss", basic_claim_t(iss)); + } + /** + * Set a subject to check for. + * Check is casesensitive. + * \param sub Subject to check for. + * \return *this to allow chaining + */ + verifier& with_subject(const typename json_traits::string_type& sub) { + return with_claim("sub", basic_claim_t(sub)); + } + /** + * Set an audience to check for. + * If any of the specified audiences is not present in the token the check fails. + * \param aud Audience to check for. + * \return *this to allow chaining + */ + verifier& with_audience(const typename basic_claim_t::set_t& aud) { + return with_claim("aud", basic_claim_t(aud)); + } + /** + * Set an audience to check for. + * If the specified audiences is not present in the token the check fails. + * \param aud Audience to check for. + * \return *this to allow chaining + */ + verifier& with_audience(const typename json_traits::string_type& aud) { + return with_claim("aud", basic_claim_t(aud)); + } + /** + * Set an id to check for. + * Check is casesensitive. + * \param id ID to check for. + * \return *this to allow chaining + */ + verifier& with_id(const typename json_traits::string_type& id) { return with_claim("jti", basic_claim_t(id)); } + /** + * Specify a claim to check for. + * \param name Name of the claim to check for + * \param c Claim to check for + * \return *this to allow chaining + */ + verifier& with_claim(const typename json_traits::string_type& name, basic_claim_t c) { + claims[name] = c; + return *this; + } + + /** + * Add an algorithm available for checking. + * \param alg Algorithm to allow + * \return *this to allow chaining + */ + template + verifier& allow_algorithm(Algorithm alg) { + algs[alg.name()] = std::make_shared>(alg); + return *this; + } + + /** + * Verify the given token. + * \param jwt Token to check + * \throw token_verification_exception Verification failed + */ + void verify(const decoded_jwt& jwt) const { + std::error_code ec; + verify(jwt, ec); + error::throw_if_error(ec); + } + /** + * Verify the given token. + * \param jwt Token to check + * \param ec error_code filled with details on error + */ + void verify(const decoded_jwt& jwt, std::error_code& ec) const { + ec.clear(); + const typename json_traits::string_type data = jwt.get_header_base64() + "." + jwt.get_payload_base64(); + const typename json_traits::string_type sig = jwt.get_signature(); + const std::string algo = jwt.get_algorithm(); + if (algs.count(algo) == 0) { + ec = error::token_verification_error::wrong_algorithm; + return; + } + algs.at(algo)->verify(data, sig, ec); + if (ec) return; + + auto assert_claim_eq = [](const decoded_jwt& jwt, const typename json_traits::string_type& key, + const basic_claim_t& c, std::error_code& ec) { + if (!jwt.has_payload_claim(key)) { + ec = error::token_verification_error::missing_claim; + return; + } + auto jc = jwt.get_payload_claim(key); + if (jc.get_type() != c.get_type()) { + ec = error::token_verification_error::claim_type_missmatch; + return; + } + if (c.get_type() == json::type::integer) { + if (c.as_date() != jc.as_date()) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + } else if (c.get_type() == json::type::array) { + auto s1 = c.as_set(); + auto s2 = jc.as_set(); + if (s1.size() != s2.size()) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + auto it1 = s1.cbegin(); + auto it2 = s2.cbegin(); + while (it1 != s1.cend() && it2 != s2.cend()) { + if (*it1++ != *it2++) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + } + } else if (c.get_type() == json::type::object) { + if (json_traits::serialize(c.to_json()) != json_traits::serialize(jc.to_json())) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + } else if (c.get_type() == json::type::string) { + if (c.as_string() != jc.as_string()) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + } else + throw std::logic_error("internal error, should be unreachable"); + }; + + auto time = clock.now(); + + if (jwt.has_expires_at()) { + auto leeway = claims.count("exp") == 1 + ? std::chrono::system_clock::to_time_t(claims.at("exp").as_date()) + : default_leeway; + auto exp = jwt.get_expires_at(); + if (time > exp + std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + return; + } + } + if (jwt.has_issued_at()) { + auto leeway = claims.count("iat") == 1 + ? std::chrono::system_clock::to_time_t(claims.at("iat").as_date()) + : default_leeway; + auto iat = jwt.get_issued_at(); + if (time < iat - std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + return; + } + } + if (jwt.has_not_before()) { + auto leeway = claims.count("nbf") == 1 + ? std::chrono::system_clock::to_time_t(claims.at("nbf").as_date()) + : default_leeway; + auto nbf = jwt.get_not_before(); + if (time < nbf - std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + return; + } + } + for (auto& c : claims) { + if (c.first == "exp" || c.first == "iat" || c.first == "nbf") { + // Nothing to do here, already checked + } else if (c.first == "aud") { + if (!jwt.has_audience()) { + ec = error::token_verification_error::audience_missmatch; + return; + } + auto aud = jwt.get_audience(); + typename basic_claim_t::set_t expected = {}; + if (c.second.get_type() == json::type::string) + expected = {c.second.as_string()}; + else + expected = c.second.as_set(); + for (auto& e : expected) { + if (aud.count(e) == 0) { + ec = error::token_verification_error::audience_missmatch; + return; + } + } + } else { + assert_claim_eq(jwt, c.first, c.second, ec); + if (ec) return; + } + } + } + }; + + /** + * Create a verifier using the given clock + * \param c Clock instance to use + * \return verifier instance + */ + template + verifier verify(Clock c) { + return verifier(c); + } + + /** + * Default clock class using std::chrono::system_clock as a backend. + */ + struct default_clock { + date now() const { return date::clock::now(); } + }; + + /** + * Return a builder instance to create a new token + */ + template + builder create() { + return builder(); + } + + /** + * Decode a token + * \param token Token to decode + * \param decode function that will pad and base64url decode the token + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const typename json_traits::string_type& token, Decode decode) { + return decoded_jwt(token, decode); + } + + /** + * Decode a token + * \param token Token to decode + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const typename json_traits::string_type& token) { + return decoded_jwt(token); + } + +#ifndef JWT_DISABLE_PICOJSON + struct picojson_traits { + using value_type = picojson::value; + using object_type = picojson::object; + using array_type = picojson::array; + using string_type = std::string; + using number_type = double; + using integer_type = int64_t; + using boolean_type = bool; + + static json::type get_type(const picojson::value& val) { + using json::type; + if (val.is()) return type::boolean; + if (val.is()) return type::integer; + if (val.is()) return type::number; + if (val.is()) return type::string; + if (val.is()) return type::array; + if (val.is()) return type::object; + + throw std::logic_error("invalid type"); + } + + static picojson::object as_object(const picojson::value& val) { + if (!val.is()) throw std::bad_cast(); + return val.get(); + } + + static std::string as_string(const picojson::value& val) { + if (!val.is()) throw std::bad_cast(); + return val.get(); + } + + static picojson::array as_array(const picojson::value& val) { + if (!val.is()) throw std::bad_cast(); + return val.get(); + } + + static int64_t as_int(const picojson::value& val) { + if (!val.is()) throw std::bad_cast(); + return val.get(); + } + + static bool as_bool(const picojson::value& val) { + if (!val.is()) throw std::bad_cast(); + return val.get(); + } + + static double as_number(const picojson::value& val) { + if (!val.is()) throw std::bad_cast(); + return val.get(); + } + + static bool parse(picojson::value& val, const std::string& str) { return picojson::parse(val, str).empty(); } + + static std::string serialize(const picojson::value& val) { return val.serialize(); } + }; + + /** + * Default JSON claim + * + * This type is the default specialization of the \ref basic_claim class which + * uses the standard template types. + */ + using claim = basic_claim; + + /** + * Create a verifier using the default clock + * \return verifier instance + */ + inline verifier verify() { + return verify(default_clock{}); + } + /** + * Return a picojson builder instance to create a new token + */ + inline builder create() { return builder(); } +#ifndef JWT_DISABLE_BASE64 + /** + * Decode a token + * \param token Token to decode + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + inline decoded_jwt decode(const std::string& token) { return decoded_jwt(token); } +#endif + /** + * Decode a token + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64url decode and + * return the results. + * \param token Token to decode + * \param decode The token to parse + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const std::string& token, Decode decode) { + return decoded_jwt(token, decode); + } +#endif +} // namespace jwt + +template +std::istream& operator>>(std::istream& is, jwt::basic_claim& c) { + return c.operator>>(is); +} + +template +std::ostream& operator<<(std::ostream& os, const jwt::basic_claim& c) { + return os << c.to_json(); +} + +#endif From b56f6e65f1b83833d6b22adefd4686bab2fdb54e Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 9 Jan 2023 19:05:08 +0000 Subject: [PATCH 002/121] Use of IS-10 register_client_response schema to validate authorization clients file --- Development/cmake/NmosCppLibraries.cmake | 1 - Development/nmos/authorization_handlers.cpp | 226 +++++++++--------- Development/nmos/authorization_handlers.h | 6 + Development/nmos/json_schema.cpp | 9 - Development/nmos/json_schema.h | 2 - .../APIs/schemas/auth_clients_schema.json | 21 -- 6 files changed, 122 insertions(+), 143 deletions(-) delete mode 100644 Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index d6e183ef9..238ab5ddf 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -695,7 +695,6 @@ set(NMOS_IS10_SCHEMAS_HEADERS set(NMOS_IS10_V1_0_TAG v1.0.x) set(NMOS_IS10_V1_0_SCHEMAS_JSON - third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/auth_clients_schema.json third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/auth_metadata.json third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/jwks_response.json third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/jwks_schema.json diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 30cc1b299..aa25576ed 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -25,25 +25,130 @@ namespace nmos static const web::json::experimental::json_validator validator { nmos::experimental::load_json_schema, - boost::copy_range>(is10_versions::all | boost::adaptors::transformed(experimental::make_auth_clients_schema_uri)) + boost::copy_range>(is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_register_client_response_uri)) }; + return validator; } } - // load the table of authorization server vs authorization client metadata from file - static web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate) + // helper function to load the authorization clients file + // example of the file + // [ + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // ] + web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate) { using web::json::value; - utility::ifstream_t is(filename); - if (is.is_open()) + try + { + utility::ifstream_t is(filename); + if (is.is_open()) + { + const auto authorization_clients = value::parse(is); + + if (!authorization_clients.is_null() && authorization_clients.is_array() && authorization_clients.as_array().size()) + { + for (auto& authorization_client : authorization_clients.as_array()) + { + if (authorization_client.has_field(nmos::experimental::fields::authorization_server_uri) && + !authorization_client.at(nmos::experimental::fields::authorization_server_uri).as_string().empty() && + authorization_client.has_field(nmos::experimental::fields::client_metadata)) + { + // validate client metadata + const auto& client_metadata = authorization_client.at(nmos::experimental::fields::client_metadata); + details::auth_clients_schema_validator().validate(client_metadata, experimental::make_authapi_register_client_response_uri(is10_versions::v1_0)); // may throw json_exception + } + } + } + + return authorization_clients; + } + } + catch (const web::json::json_exception& e) { - return value::parse(is); + slog::log(gate, SLOG_FLF) << "Unable to load authorization clients from non-volatile memory: " << filename << ": " << e.what(); } return web::json::value::array(); } + // helper function to update the authorization clients file + // example of authorization_client + // { + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "issuer" : "https://example.com", + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // } + void update_authorization_clients_file(const utility::string_t& filename, const web::json::value& authorization_client, slog::base_gate& gate) + { + // load authorization_clients from file + auto authorization_clients = load_authorization_clients_file(filename, gate); + + // update/append to the authorization_clients + bool updated{ false }; + if (authorization_clients.as_array().size()) + { + for (auto& auth_client : authorization_clients.as_array()) + { + const auto& authorization_server_uri = auth_client.at(nmos::experimental::fields::authorization_server_uri); + if (authorization_server_uri == authorization_client.at(nmos::experimental::fields::authorization_server_uri)) + { + auth_client[nmos::experimental::fields::client_metadata] = authorization_client.at(nmos::experimental::fields::client_metadata); + updated = true; + break; + } + } + } + if (!updated) + { + web::json::push_back(authorization_clients, authorization_client); + } + + // save the updated authorization_clients to file + utility::ofstream_t os(filename, std::ios::out | std::ios::trunc); + if (os.is_open()) + { + os << authorization_clients.serialize(); + os.close(); + } + } + // construct callback to load a table of authorization server uri vs authorization client metadata from file based on settings seed_id // it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs) load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate) @@ -53,45 +158,9 @@ namespace nmos // obtain client metadata from the safe, permission-restricted, location in the non-volatile memory, e.g. a file // Client metadata SHOULD consist of the client_id, client_secret, client_secret_expires_at, client_uri, grant_types, redirect_uris, response_types, scope, token_endpoint_auth_method auto filename = nmos::experimental::fields::seed_id(settings) + U(".json"); - slog::log(gate, SLOG_FLF) << "Load authorization client from non-volatile memory: " << filename; + slog::log(gate, SLOG_FLF) << "Load authorization clients from non-volatile memory: " << filename; - // example of the authorization client file - // [ - // { - // "authorization_server_uri": "https://example.com" - // }, - // { - // "client_metadata": { - // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", - // "client_name" : "example client", - // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], - // "jwks_uri" : "https://example_client/jwks", - // "redirect_uris" : [ "https://example_client/callback" ], - // "registration_access_token" : "eyJhbGci....", - // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", - // "response_types" : [ "code" ], - // "scope" : "registration", - // "subject_type" : "public", - // "tls_client_certificate_bound_access_tokens" : false, - // "token_endpoint_auth_method" : "private_key_jwt" - // } - // } - // ] - - try - { - const auto authorization_clients = load_authorization_clients_file(filename, gate); - - details::auth_clients_schema_validator().validate(authorization_clients, experimental::make_auth_clients_schema_uri(is10_versions::v1_0)); - - return authorization_clients; - } - catch (const web::json::json_exception& e) - { - slog::log(gate, SLOG_FLF) << "Unable to load authorization client from non-volatile memory: " << filename << ": " << e.what(); - - return web::json::value::array(); - } + return load_authorization_clients_file(filename, gate); }; } @@ -101,76 +170,13 @@ namespace nmos { return [&](const web::json::value& authorization_client) { - // Client metadata SHOULD be stored in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. + // Client metadata SHOULD be stored in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent re-registration. // Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials const auto filename = nmos::experimental::fields::seed_id(settings) + U(".json"); - slog::log(gate, SLOG_FLF) << "Save authorization client to non-volatile memory: " << filename; - - // exmaple of authorization client JSON - // { - // { - // "authorization_server_uri": "https://example.com" - // }, - // { - // "client_metadata": { - // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", - // "client_name" : "example client", - // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], - // "issuer" : "https://example.com", - // "jwks_uri" : "https://example_client/jwks", - // "redirect_uris" : [ "https://example_client/callback" ], - // "registration_access_token" : "eyJhbGci....", - // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", - // "response_types" : [ "code" ], - // "scope" : "registration", - // "subject_type" : "public", - // "tls_client_certificate_bound_access_tokens" : false, - // "token_endpoint_auth_method" : "private_key_jwt" - // } - // } - // } - - // load authorization_clients from file - web::json::value authorization_clients; - try - { - authorization_clients = load_authorization_clients_file(filename, gate); - } - catch (const web::json::json_exception& e) - { - slog::log(gate, SLOG_FLF) << "Unable to load authorization client from non-volatile memory: " << filename << ": " << e.what(); + slog::log(gate, SLOG_FLF) << "Save authorization clients to non-volatile memory: " << filename; - authorization_clients = web::json::value::array(); - } - - // insert client to authorization_clients - bool inserted{ false }; - if (authorization_clients.as_array().size()) - { - for (auto& setting : authorization_clients.as_array()) - { - const auto& authorization_server_uri = setting.at(nmos::experimental::fields::authorization_server_uri); - if (authorization_server_uri == authorization_client.at(nmos::experimental::fields::authorization_server_uri)) - { - setting[nmos::experimental::fields::client_metadata] = authorization_client.at(nmos::experimental::fields::client_metadata); - inserted = true; - break; - } - } - } - if (!inserted) - { - web::json::push_back(authorization_clients, authorization_client); - } - - // save the updated authorization_clients to file - utility::ofstream_t os(filename, std::ios::out | std::ios::trunc); - if (os.is_open()) - { - os << authorization_clients.serialize(); - os.close(); - } + update_authorization_clients_file(filename, authorization_client, gate); }; } diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h index 4d238c6c7..cdbde092d 100644 --- a/Development/nmos/authorization_handlers.h +++ b/Development/nmos/authorization_handlers.h @@ -100,6 +100,12 @@ namespace nmos // this callback should not throw exceptions typedef std::function request_authorization_code_handler; + // helper function to load from the authorization clients file + web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate); + + // helper function to update the authorization clients file + void update_authorization_clients_file(const utility::string_t& filename, const web::json::value& authorization_client, slog::base_gate& gate); + // construct callback to load a table of authorization server uri vs authorization clients metadata from file based on settings seed_id load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate); diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index f59ae42db..c88045ef2 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -140,8 +140,6 @@ namespace nmos using namespace nmos::is10_schemas::v1_0_x; const utility::string_t tag(_XPLATSTR("v1.0.x")); - const web::uri auth_clients_schema_uri = make_schema_uri(tag, _XPLATSTR("auth_clients_schema.json")); - const web::uri authapi_auth_metadata_schema_uri = make_schema_uri(tag, _XPLATSTR("auth_metadata.json")); const web::uri authapi_jwks_response_schema_uri = make_schema_uri(tag, _XPLATSTR("jwks_response.json")); const web::uri authapi_register_client_error_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_error_response.json")); @@ -341,8 +339,6 @@ namespace nmos return { // v1.0 - { make_schema_uri(v1_0::tag, _XPLATSTR("auth_clients_schema.json")), make_schema(v1_0::auth_clients_schema) }, - { make_schema_uri(v1_0::tag, _XPLATSTR("auth_metadata.json")), make_schema(v1_0::auth_metadata) }, { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_response.json")), make_schema(v1_0::jwks_response) }, { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_schema.json")), make_schema(v1_0::jwks_schema) }, @@ -426,11 +422,6 @@ namespace nmos return is08_schemas::v1_0::map_activations_post_request_uri; } - web::uri make_auth_clients_schema_uri(const nmos::api_version& version) - { - return is10_schemas::v1_0::auth_clients_schema_uri; - } - web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version) { return is10_schemas::v1_0::authapi_auth_metadata_schema_uri; diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index d71ae9940..e74ae19cd 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -29,8 +29,6 @@ namespace nmos web::uri make_channelmappingapi_map_activations_post_request_schema_uri(const nmos::api_version& version); - web::uri make_auth_clients_schema_uri(const nmos::api_version& version); - web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version); web::uri make_authapi_jwks_response_schema_uri(const nmos::api_version& version); web::uri make_authapi_register_client_response_uri(const nmos::api_version& version); diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json deleted file mode 100644 index 79c68bb1c..000000000 --- a/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_clients_schema.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Authorization clients table", - "description": "Authorization client for the different Auhorization server", - "type": "array", - "items": { - "type": "object", - "required": ["authorization_server_uri", "client_metadata"], - "properties": { - "authorization_server_uri": { - "description": "The authorization server's issuer identifier, which is a URL that uses the 'https' scheme and has no query or fragment components.", - "format": "uri", - "type": "string", - "minLength": 1 - }, - "client_metadata" : { - "$ref": "register_client_response.json" - } - } - } -} From 3fbda5e6732825851883e7aa1fc12adcd80cac2f Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 24 Jan 2023 11:10:38 +0000 Subject: [PATCH 003/121] Optimize and clean up authorization validation --- Development/nmos/api_utils.cpp | 2 +- Development/nmos/authorization.cpp | 71 ++++++++++++------------- Development/nmos/authorization.h | 4 +- Development/nmos/events_ws_api.cpp | 2 +- Development/nmos/jwt_validator.h | 6 +-- Development/nmos/jwt_validator_impl.cpp | 50 ++++------------- Development/nmos/ws_api_utils.cpp | 2 +- 7 files changed, 52 insertions(+), 85 deletions(-) diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 659064cd2..358aae25d 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -780,7 +780,7 @@ namespace nmos const auto audience = with_read_lock(model.mutex, [&] { const auto& settings = model.settings; return nmos::get_host_name(settings); }); auto error = with_write_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &gate_] { - return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, authorization_state.authorization_server_uri, authorization_state.token_issuer, gate_); + return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, authorization_state.token_issuer, gate_); }); if (error) diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp index 0586cf417..e8dbace7d 100644 --- a/Development/nmos/authorization.cpp +++ b/Development/nmos/authorization.cpp @@ -24,9 +24,9 @@ namespace nmos try { - const auto& token_issuer = nmos::experimental::jwt_validator::token_issuer(access_token); + const auto& token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); - // is token from expected issuer + // is token from the expected issuer if (token_issuer == expected_issuer) { // is token expired @@ -65,7 +65,7 @@ namespace nmos } const auto access_token = token.substr(scheme.length()); - return jwt_validator::client_id(access_token); + return jwt_validator::get_client_id(access_token); } catch (const std::exception& e) { @@ -74,62 +74,57 @@ namespace nmos return{}; } - authorization_error validate_authorization(const utility::string_t& access_token, const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate) + authorization_error validate_authorization(const utility::string_t& access_token, const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate) { if (access_token.empty()) { slog::log(gate, SLOG_FLF) << "missing access token"; return{ authorization_error::without_authentication, "missing access token" }; } - - if (issuers.empty()) + + try + { + // extract the token issuer from the token + token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); + } + catch (const std::exception& e) { - try - { - // record the unknown issuer of this access token, i.e. no public keys to validate the access token - // this will be used in the authorization_token_issuer_thread to fetch the missing public keys for token validation - token_issuer = nmos::experimental::jwt_validator::token_issuer(access_token); -#if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << "no public keys to validate access token"; -#else - slog::log(gate, SLOG_FLF) << "no public keys to validate access token: " << access_token; -#endif - return{ authorization_error::no_matching_keys, "no public keys to validate access token" }; - } - catch (const std::exception& e) - { #if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << "invalid token issuer: " << e.what(); + slog::log(gate, SLOG_FLF) << "unable to extract token issuer from access token: " << e.what(); #else - slog::log(gate, SLOG_FLF) << "invalid token issuer: " << e.what() << "; access_token: " << access_token; + slog::log(gate, SLOG_FLF) << "unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token; #endif - return{ authorization_error::failed, e.what() }; - } + return{ authorization_error::failed, e.what() }; } - + + // find the relevent issuer's public keys to validate the token std::string error; - for (auto issuer = issuers.begin(); issuer != issuers.end(); issuer++) + auto issuer = issuers.find(token_issuer); + if(issuers.end() != issuer) { + slog::log(gate, SLOG_FLF) << "validate access token against " << utility::us2s(issuer->first.to_string()) << " public keys"; + try { // if jwt_validator has not already set up, treat it as no public keys to validate token if (issuer->second.jwt_validator.is_initialized()) { - issuer->second.jwt_validator.validate(access_token, request, scope, audience, auth_server); + issuer->second.jwt_validator.validate(access_token, request, scope, audience); return{ authorization_error::succeeded }; } + else + { + slog::log(gate, SLOG_FLF) << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; + } } catch (const no_matching_keys_exception& e) { - // validator failed to decode token due to no valid public keys, try next set of issuer's validator - // this will be used in the authorization_token_issuer_thread to fetch the missing public keys for token validation - token_issuer = e.issuer; error = e.what(); #if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; + slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; #else - slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; -#endif + slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; +#endif } catch (const insufficient_scope_exception& e) { @@ -153,7 +148,7 @@ namespace nmos return{ authorization_error::no_matching_keys, error }; } - authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate) + authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate) { try { @@ -173,7 +168,7 @@ namespace nmos } const auto access_token = token.substr(scheme.length()); - return validate_authorization(access_token, issuers, request, scope, audience, auth_server, token_issuer, gate); + return validate_authorization(access_token, issuers, request, scope, audience, token_issuer, gate); } catch (const without_authentication_exception& e) { @@ -185,9 +180,9 @@ namespace nmos // Clients SHOULD use the "Authorization Request Header Field" method. // Clients MAY use "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 - authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate) + authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate) { - auto error = validate_authorization(issuers, request, scope, audience, auth_server, token_issuer, gate); + auto error = validate_authorization(issuers, request, scope, audience, token_issuer, gate); if (error) { @@ -201,7 +196,7 @@ namespace nmos auto it = querys.find(U("access_token")); if (querys.end() != it) { - error = nmos::experimental::validate_authorization(it->second, issuers, request, scope, audience, auth_server, token_issuer, gate); + error = nmos::experimental::validate_authorization(it->second, issuers, request, scope, audience, token_issuer, gate); } } } diff --git a/Development/nmos/authorization.h b/Development/nmos/authorization.h index 78a7e7ddf..6e35b57ba 100644 --- a/Development/nmos/authorization.h +++ b/Development/nmos/authorization.h @@ -52,13 +52,13 @@ namespace nmos utility::string_t get_client_id(const web::http::http_headers& headers, slog::base_gate& gate); - authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate); + authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate); // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket // Clients SHOULD use the "Authorization Request Header Field" method. // Clients MAY use "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 - authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server, web::uri& token_issuer, slog::base_gate& gate); + authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate); } } diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index d56d61f53..e1fb8f126 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -79,7 +79,7 @@ namespace nmos const auto& settings = model.settings; authorization_state.write_lock(); - const auto error = nmos::experimental::ws_validate_authorization(authorization_state.issuers, req, nmos::experimental::scopes::events, nmos::get_host_name(settings), authorization_state.authorization_server_uri, authorization_state.token_issuer, gate_); + const auto error = nmos::experimental::ws_validate_authorization(authorization_state.issuers, req, nmos::experimental::scopes::events, nmos::get_host_name(settings), authorization_state.token_issuer, gate_); if (error) { // set error repsonse diff --git a/Development/nmos/jwt_validator.h b/Development/nmos/jwt_validator.h index 9aec9afe0..958be2871 100644 --- a/Development/nmos/jwt_validator.h +++ b/Development/nmos/jwt_validator.h @@ -51,10 +51,10 @@ namespace nmos bool is_initialized() const; void validate_expiry(const utility::string_t& token) const; - void validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server) const; + void validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) const; - static utility::string_t client_id(const utility::string_t& token); - static web::uri token_issuer(const utility::string_t& token); + static utility::string_t get_client_id(const utility::string_t& token); + static web::uri get_token_issuer(const utility::string_t& token); private: std::shared_ptr impl; diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index df476b210..c65e73e13 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -100,7 +100,7 @@ namespace nmos throw std::runtime_error("no public keys to validate access token expiry"); } - void validate(const utility::string_t& token, const web::http::http_request& req, const scope& scope, const utility::string_t& audience, const web::uri& auth_server) const + void validate(const utility::string_t& token, const web::http::http_request& req, const scope& scope, const utility::string_t& audience) const { using namespace jwt::experimental::details; @@ -357,11 +357,6 @@ namespace nmos } } } - else - { - // no public keys to validate token - errors.push_back("no public keys to validate access token"); - } // reaching here, there must be no matching public key for the token @@ -373,35 +368,12 @@ namespace nmos // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys const auto token_issuer = web::uri{ utility::s2us(decoded_token.get_issuer()) }; - - // if token is coming from an unknown issuer, do public keys fetch on token issuer, otherwise failed with no public keys! - if (!auth_server.has_same_authority(token_issuer)) - { - // verify token issuer - if (token_issuer.scheme() != web::uri_schemes::http && token_issuer.scheme() != web::uri_schemes::https) - { - errors.push_back("issuer must be 'http' or 'https'"); - throw std::runtime_error(format_errors(errors)); - } - - if (token_issuer.host().empty()) - { - errors.push_back("issuer must contain a hostname"); - throw std::runtime_error(format_errors(errors)); - } - - // no matching public keys for the token, re-fetch public keys from token issuer - throw no_matching_keys_exception(token_issuer, format_errors(errors)); - } - else - { - // no public keys to validate token - throw std::runtime_error("no public keys to validate access token"); - } + // no matching public keys for the token, re-fetch public keys from token issuer + throw no_matching_keys_exception(token_issuer, format_errors(errors)); } // may throw - static utility::string_t client_id(const utility::string_t& token) + static utility::string_t get_client_id(const utility::string_t& token) { using namespace jwt::experimental::details; @@ -429,7 +401,7 @@ namespace nmos } // may throw - static web::uri token_issuer(const utility::string_t& token) + static web::uri get_token_issuer(const utility::string_t& token) { using namespace jwt::experimental::details; @@ -474,21 +446,21 @@ namespace nmos impl->validate_expiry(token); } - void jwt_validator::validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, const web::uri& auth_server) const + void jwt_validator::validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) const { if (!impl) { throw std::runtime_error("JWT validator has not initiliased"); } - impl->validate(token, request, scope, audience, auth_server); + impl->validate(token, request, scope, audience); } - utility::string_t jwt_validator::client_id(const utility::string_t& token) + utility::string_t jwt_validator::get_client_id(const utility::string_t& token) { - return details::jwt_validator_impl::client_id(token); + return details::jwt_validator_impl::get_client_id(token); } - web::uri jwt_validator::token_issuer(const utility::string_t& token) + web::uri jwt_validator::get_token_issuer(const utility::string_t& token) { - return details::jwt_validator_impl::token_issuer(token); + return details::jwt_validator_impl::get_token_issuer(token); } } } diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp index d66c3a6d2..2c674188d 100644 --- a/Development/nmos/ws_api_utils.cpp +++ b/Development/nmos/ws_api_utils.cpp @@ -21,7 +21,7 @@ namespace nmos const auto& settings = model.settings; authorization_state.write_lock(); - const auto error = ws_validate_authorization(authorization_state.issuers, request, scope, nmos::get_host_name(settings), authorization_state.authorization_server_uri, authorization_state.token_issuer, gate); + const auto error = ws_validate_authorization(authorization_state.issuers, request, scope, nmos::get_host_name(settings), authorization_state.token_issuer, gate); if (error) { // set error repsonse From 52935e95c1cb837536216bc8d78270487f2a2980 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 14 Feb 2023 11:54:43 +0000 Subject: [PATCH 004/121] Fix typo --- Development/nmos/authorization_handlers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index aa25576ed..6a2281573 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -194,7 +194,7 @@ namespace nmos auto browser_cmd(U("xdg-open \"") + authorization_code_uri.to_string() + U("\"")); if (0 > system(browser_cmd.c_str())) { - slog::log(gate, SLOG_FLF) << "Faile to open a browser to start the authorization code flow"; + slog::log(gate, SLOG_FLF) << "Failed to open a browser to start the authorization code flow"; } #endif }; From 51f145ee7341a80bfc77f8025f76b541dc7cf284 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 14 Feb 2023 11:55:28 +0000 Subject: [PATCH 005/121] Use the matching keyid between the jwks and the client assertion --- Development/nmos/authorization_operation.cpp | 10 +++++----- Development/nmos/authorization_redirect_api.cpp | 12 +++++++++--- Development/nmos/jwks_uri_api.cpp | 4 +++- Development/nmos/jwt_generator.h | 2 +- Development/nmos/jwt_generator_impl.cpp | 11 ++++++----- 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 5d25c8014..fb3c39e08 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -918,23 +918,23 @@ namespace nmos auto fetch_time = std::chrono::steady_clock::now(); return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &model , &token_state, &gate]() { - // create client assertion for private key jwt + // create client assertion using private key jwt utility::string_t client_assertion; with_read_lock(model.mutex, [&] { if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) { - // get RSA private key from list + // use the 1st RSA private key from RSA private keys list to create the client_assertion if (!token_state.load_rsa_private_keys) { throw web::http::oauth2::experimental::oauth2_exception(U("missing RSA private key loader to extract RSA private key")); } - auto rsa_private_key = details::found_rsa_key(token_state.load_rsa_private_keys()); - if (rsa_private_key.empty()) + auto rsa_private_keys = token_state.load_rsa_private_keys(); + if (rsa_private_keys.empty() || rsa_private_keys[0].empty()) { throw web::http::oauth2::experimental::oauth2_exception(U("no RSA key to create client assertion")); } - client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key); + client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_keys[0], U("1")); } }); diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp index 15334a033..27b17637f 100644 --- a/Development/nmos/authorization_redirect_api.cpp +++ b/Development/nmos/authorization_redirect_api.cpp @@ -79,6 +79,7 @@ namespace nmos utility::string_t redirect_uri; utility::string_t token_endpoint_auth_method; utility::string_t rsa_private_key; + utility::string_t keyid; auto client_assertion_lifespan(std::chrono::seconds(30)); with_read_lock(model.mutex, [&, load_ca_certificates, load_rsa_private_keys] { @@ -95,8 +96,13 @@ namespace nmos if (load_rsa_private_keys) { - // get RSA private key from list - rsa_private_key = details::found_rsa_key(load_rsa_private_keys()); + // use the 1st RSA private key from RSA private keys list to create the client_assertion + auto rsa_private_keys = load_rsa_private_keys(); + if (!rsa_private_keys.empty()) + { + rsa_private_key = rsa_private_keys[0]; + keyid = U("1"); + } } client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings)); }); @@ -107,7 +113,7 @@ namespace nmos if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) { - const auto client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key); + const auto client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key, keyid); // exchange authorization code for bearer token // where redirected URI: /x-authorization/callback/?state=&code= diff --git a/Development/nmos/jwks_uri_api.cpp b/Development/nmos/jwks_uri_api.cpp index bd1b0a7b2..cc44dc333 100644 --- a/Development/nmos/jwks_uri_api.cpp +++ b/Development/nmos/jwks_uri_api.cpp @@ -42,9 +42,11 @@ namespace nmos rsa_private_keys = load_rsa_private_keys(); }); + int idx = 0; for (const auto rsa_private_key : rsa_private_keys) { - const auto jwk = details::private_key_to_jwk(rsa_private_key, make_id()); + const auto keyid = std::to_string(++idx); + const auto jwk = details::private_key_to_jwk(rsa_private_key, utility::s2us(keyid)); web::json::push_back(keys, jwk); } diff --git a/Development/nmos/jwt_generator.h b/Development/nmos/jwt_generator.h index 71b27902e..d5f8dd38e 100644 --- a/Development/nmos/jwt_generator.h +++ b/Development/nmos/jwt_generator.h @@ -10,7 +10,7 @@ namespace nmos class jwt_generator { public: - static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key); + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid); }; } } diff --git a/Development/nmos/jwt_generator_impl.cpp b/Development/nmos/jwt_generator_impl.cpp index 154e5d7e5..fc24c77d2 100644 --- a/Development/nmos/jwt_generator_impl.cpp +++ b/Development/nmos/jwt_generator_impl.cpp @@ -14,7 +14,7 @@ namespace nmos class jwt_generator_impl { public: - static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& public_key, const utility::string_t& private_key) + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& public_key, const utility::string_t& private_key, const utility::string_t& keyid) { using namespace jwt::experimental::details; @@ -29,22 +29,23 @@ namespace nmos .set_issued_at(std::chrono::system_clock::now()) .set_expires_at(std::chrono::system_clock::now() + token_lifetime) .set_id(utility::us2s(nmos::make_id())) + .set_key_id(utility::us2s(keyid)) .set_type("JWT") .sign(jwt::algorithm::rs256(utility::us2s(public_key), utility::us2s(private_key)))); } - static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key) + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid) { - return create_client_assertion(issuer, subject, audience, token_lifetime, rsa_public_key(private_key), private_key); + return create_client_assertion(issuer, subject, audience, token_lifetime, rsa_public_key(private_key), private_key, keyid); } }; } - utility::string_t jwt_generator::create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key) + utility::string_t jwt_generator::create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid) { try { - return details::jwt_generator_impl::create_client_assertion(issuer, subject, audience, token_lifetime, private_key); + return details::jwt_generator_impl::create_client_assertion(issuer, subject, audience, token_lifetime, private_key, keyid); } catch (const jwt::error::signature_generation_exception& e) { From ab035caab4ba770fa450d4cfb593c080de1daf78 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 20 Feb 2023 14:12:06 +0000 Subject: [PATCH 006/121] Add Access Token Error response handler for the Authorization Code Grant, and general tidy up --- Development/cmake/NmosCppLibraries.cmake | 1 + Development/cpprest/access_token_error.h | 67 ++++++ Development/cpprest/client_type.h | 12 - Development/cpprest/grant_type.h | 7 +- Development/cpprest/resource_server_error.h | 13 -- Development/cpprest/response_type.h | 13 -- .../cpprest/token_endpoint_auth_method.h | 18 +- Development/nmos/authorization_handlers.cpp | 14 +- Development/nmos/authorization_operation.cpp | 221 ++++++++++-------- Development/nmos/authorization_operation.h | 20 +- .../nmos/authorization_redirect_api.cpp | 182 ++++++++++++++- Development/nmos/authorization_utils.cpp | 4 +- Development/nmos/json_schema.cpp | 7 + Development/nmos/json_schema.h | 1 + Development/nmos/jwks_uri_api.cpp | 2 - Development/nmos/jwt_generator_impl.cpp | 9 +- Development/nmos/settings.h | 2 +- 17 files changed, 403 insertions(+), 190 deletions(-) create mode 100644 Development/cpprest/access_token_error.h diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 238ab5ddf..6bb2df7a1 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -790,6 +790,7 @@ if(MSVC) endif() set(NMOS_CPP_CPPREST_HEADERS + cpprest/access_token_error.h cpprest/api_router.h cpprest/basic_utils.h cpprest/client_type.h diff --git a/Development/cpprest/access_token_error.h b/Development/cpprest/access_token_error.h new file mode 100644 index 000000000..6ed7ceed0 --- /dev/null +++ b/Development/cpprest/access_token_error.h @@ -0,0 +1,67 @@ +#ifndef CPPREST_ACCESS_TOKEN_ERROR_H +#define CPPREST_ACCESS_TOKEN_ERROR_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + // for redirect error + // "If the resource owner denies the access request or if the request + // fails for reasons other than a missing or invalid redirection URI, + // the authorization server informs the client by adding the following + // parameters to the query component of the redirection URI using the + // "application/x-www-form-urlencoded" format" + // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + + // for diret error + // If the access token request is invalid or unauthorized + // "The authorization server responds with an HTTP 400 (Bad Request) + // status code(unless specified otherwise) and includes the following + // parameters with the response:" + // and https://tools.ietf.org/html/rfc6749#section-5.2 + // "The parameters are included in the entity-body of the HTTP response + // using the "application/json" media type" + + DEFINE_STRING_ENUM(access_token_error) + namespace access_token_errors + { + const access_token_error invalid_request{ U("invalid_request") }; // used for redirect error and direct error + const access_token_error unauthorized_client{ U("unauthorized_client") }; // used for redirect error and direct error + const access_token_error access_denied{ U("access_denied") }; // used for redirect error + const access_token_error unsupported_response_type{ U("unsupported_response_type") }; // used for redirect error + const access_token_error invalid_scope{ U("invalid_scope") }; // used for redirect error and direct error + const access_token_error server_error{ U("server_error") }; // used for redirect error + const access_token_error temporarily_unavailable{ U("temporarily_unavailable") }; // used for redirect error + const access_token_error invalid_client{ U("invalid_client") }; // used for direct error + const access_token_error invalid_grant{ U("invalid_grant") }; // used for direct error + const access_token_error unsupported_grant_type{ U("unsupported_grant_type") }; // used for direct error + } + + inline access_token_error to_access_token_error(const utility::string_t& error) + { + using namespace access_token_errors; + if (invalid_request.name == error) { return invalid_request; } + if (unauthorized_client.name == error) { return unauthorized_client; } + if (access_denied.name == error) { return access_denied; } + if (unsupported_response_type.name == error) { return unsupported_response_type; } + if (invalid_scope.name == error) { return invalid_scope; } + if (server_error.name == error) { return server_error; } + if (temporarily_unavailable.name == error) { return temporarily_unavailable; } + if (invalid_client.name == error) { return invalid_client; } + if (invalid_grant.name == error) { return invalid_grant; } + if (unsupported_grant_type.name == error) { return unsupported_grant_type; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/client_type.h b/Development/cpprest/client_type.h index d72fa169b..c23ca9eab 100644 --- a/Development/cpprest/client_type.h +++ b/Development/cpprest/client_type.h @@ -19,18 +19,6 @@ namespace web const client_type confidential_client{ U("confidential_client") }; const client_type public_client{ U("public_client") }; } - - inline utility::string_t make_client_type(const client_type& type) - { - return type.name; - } - - inline client_type parse_client_type(const utility::string_t& type) - { - if (client_types::confidential_client.name == type) { return client_types::confidential_client; } - if (client_types::public_client.name == type) { return client_types::public_client; } - return{}; - } } } } diff --git a/Development/cpprest/grant_type.h b/Development/cpprest/grant_type.h index d729a5219..bddfe7cf6 100644 --- a/Development/cpprest/grant_type.h +++ b/Development/cpprest/grant_type.h @@ -26,12 +26,7 @@ namespace web const grant_type device_code{ U("urn:ietf:params:oauth:grant-type:device_code") }; } - inline utility::string_t make_grant(const grant_type& grant) - { - return grant.name; - } - - inline grant_type parse_grant(const utility::string_t& grant) + inline grant_type to_grant_type(const utility::string_t& grant) { if (grant_types::authorization_code.name == grant) { return grant_types::authorization_code; } if (grant_types::implicit.name == grant) { return grant_types::implicit; } diff --git a/Development/cpprest/resource_server_error.h b/Development/cpprest/resource_server_error.h index 2dc599b59..bf790ca25 100644 --- a/Development/cpprest/resource_server_error.h +++ b/Development/cpprest/resource_server_error.h @@ -23,19 +23,6 @@ namespace web const resource_server_error invalid_token{ U("invalid_token") }; const resource_server_error insufficient_scope{ U("insufficient_scope") }; } - - inline utility::string_t make_resource_server_error(const resource_server_error& error) - { - return error.name; - } - - inline resource_server_error parse_resource_server_error(const utility::string_t& error) - { - if (resource_server_errors::invalid_request.name == error) { return resource_server_errors::invalid_request; } - if (resource_server_errors::invalid_token.name == error) { return resource_server_errors::invalid_token; } - if (resource_server_errors::insufficient_scope.name == error) { return resource_server_errors::insufficient_scope; } - return{}; - } } } } diff --git a/Development/cpprest/response_type.h b/Development/cpprest/response_type.h index 27347a2a0..8946f2029 100644 --- a/Development/cpprest/response_type.h +++ b/Development/cpprest/response_type.h @@ -20,19 +20,6 @@ namespace web const response_type code{ U("code") }; const response_type token{ U("token") }; } - - inline utility::string_t make_response_type(const response_type& response_type) - { - return response_type.name; - } - - inline response_type parse_response_type(const utility::string_t& response_type) - { - if (response_types::none.name == response_type) { return response_types::none; } - if (response_types::code.name == response_type) { return response_types::code; } - if (response_types::token.name == response_type) { return response_types::token; } - return{}; - } } } } diff --git a/Development/cpprest/token_endpoint_auth_method.h b/Development/cpprest/token_endpoint_auth_method.h index c6267f351..06299aed3 100644 --- a/Development/cpprest/token_endpoint_auth_method.h +++ b/Development/cpprest/token_endpoint_auth_method.h @@ -25,18 +25,14 @@ namespace web const token_endpoint_auth_method client_secret_jwt{ U("client_secret_jwt") }; } - inline utility::string_t make_token_endpoint_auth_method(const token_endpoint_auth_method& token_endpoint_auth_method) + inline token_endpoint_auth_method to_token_endpoint_auth_method(const utility::string_t& token_endpoint_auth_method) { - return token_endpoint_auth_method.name; - } - - inline token_endpoint_auth_method parse_token_endpoint_auth_method(const utility::string_t& token_endpoint_auth_method) - { - if (token_endpoint_auth_method == token_endpoint_auth_methods::client_secret_basic.name) { return token_endpoint_auth_methods::client_secret_basic; } - if (token_endpoint_auth_method == token_endpoint_auth_methods::client_secret_post.name) { return token_endpoint_auth_methods::client_secret_post; } - if (token_endpoint_auth_method == token_endpoint_auth_methods::none.name) { return token_endpoint_auth_methods::none; } - if (token_endpoint_auth_method == token_endpoint_auth_methods::private_key_jwt.name) { return token_endpoint_auth_methods::private_key_jwt; } - if (token_endpoint_auth_method == token_endpoint_auth_methods::client_secret_jwt.name) { return token_endpoint_auth_methods::client_secret_jwt; } + using namespace token_endpoint_auth_methods; + if (token_endpoint_auth_method == client_secret_basic.name) { return client_secret_basic; } + if (token_endpoint_auth_method == client_secret_post.name) { return client_secret_post; } + if (token_endpoint_auth_method == none.name) { return none; } + if (token_endpoint_auth_method == private_key_jwt.name) { return private_key_jwt; } + if (token_endpoint_auth_method == client_secret_jwt.name) { return client_secret_jwt; } return {}; } } diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 6a2281573..ebbf56892 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -181,22 +181,28 @@ namespace nmos } // construct callback to start the authorization code flow request on a browser - // it is required for OAuth client which is using the Authorization Code Flow to obtain the token + // it is required for those OAuth client which is using the Authorization Code Flow to obtain the access token + // note: as it is not easy to specify the 'content-type' used in the browser programmatically, this can be easily + // fixed by installing a browser header modifier + // such extension e.g. ModHeader can be used to add the missing 'content-type' header accordingly + // for Windows https://chrome.google.com/webstore/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj + // for Linux https://addons.mozilla.org/en-GB/firefox/addon/modheader-firefox/ request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate) { return[&gate](const web::uri& authorization_code_uri) { slog::log(gate, SLOG_FLF) << "Open a browser to start the authorization code flow: " << authorization_code_uri.to_string(); + std::string browser_cmd; #if defined(_WIN32) && !defined(__cplusplus_winrt) - ShellExecuteA(NULL, "open", utility::us2s(authorization_code_uri.to_string()).c_str(), NULL, NULL, SW_SHOWNORMAL); + browser_cmd = "start \"\" \"" + utility::us2s(authorization_code_uri.to_string()) + "\""; #else - auto browser_cmd(U("xdg-open \"") + authorization_code_uri.to_string() + U("\"")); + browser_cmd = "xdg-open \"" + utility::us2s(authorization_code_uri.to_string()) + "\""; +#endif if (0 > system(browser_cmd.c_str())) { slog::log(gate, SLOG_FLF) << "Failed to open a browser to start the authorization code flow"; } -#endif }; } diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index fb3c39e08..aaf67daee 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -1,5 +1,6 @@ #include "nmos/authorization_operation.h" +#include #include #include "cpprest/code_challenge_method.h" #include "cpprest/json_validator.h" @@ -31,10 +32,11 @@ namespace nmos static const web::json::experimental::json_validator validator { nmos::experimental::load_json_schema, - boost::copy_range>(boost::join(boost::join(boost::join(boost::join( + boost::copy_range>(boost::join(boost::join(boost::join(boost::join(boost::join( is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_auth_metadata_schema_uri), is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_jwks_response_schema_uri)), is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_register_client_response_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_error_response_uri)), is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_response_schema_uri)), is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_schema_schema_uri))) }; @@ -649,11 +651,11 @@ namespace nmos } else { -#if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: " << response.status_code() << " " << response.reason_phrase(); -#else - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: " << response.status_code() << " " << response.reason_phrase() << " " << utility::us2s(body.serialize()); -#endif + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: " << response.status_code() << " " << utility::us2s(body.serialize()); + + // validate token error response JSON + authapi_validator().validate(body, experimental::make_authapi_token_error_response_uri(version)); // may throw json_exception + throw authorization_exception(); } }); @@ -695,12 +697,8 @@ namespace nmos return request_token(client, version, ub, client_id, {}, scope, gate, token); } - // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token - pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + web::uri_builder make_request_token_base_query(const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier) { - slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with code_verifier: " << utility::us2s(code_verifier); - - using namespace web::http; using web::http::oauth2::details::oauth2_strings; web::uri_builder ub; @@ -708,24 +706,25 @@ namespace nmos ub.append_query(oauth2_strings::code, web::uri::encode_data_string(code), false); ub.append_query(oauth2_strings::redirect_uri, web::uri::encode_data_string(redirect_uri), false); ub.append_query(U("code_verifier"), code_verifier, false); + return ub; + } + + // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with code_verifier: " << utility::us2s(code_verifier); + + auto ub = make_request_token_base_query(code, redirect_uri, code_verifier); return request_token(client, version, ub, client_id, client_secret, scope, gate, token); } // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication - pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, const utility::string_t& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + pplx::task request_token_from_authorization_code_with_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with " << utility::us2s(token_endpoint_auth_method) << " and code_verifier: "<< utility::us2s(code_verifier) << " and client_assertion: " << utility::us2s(client_assertion); - - using namespace web::http; - using web::http::oauth2::details::oauth2_strings; - - web::uri_builder ub; - ub.append_query(oauth2_strings::grant_type, oauth2_strings::authorization_code, false); - ub.append_query(oauth2_strings::code, web::uri::encode_data_string(code), false); - ub.append_query(oauth2_strings::redirect_uri, web::uri::encode_data_string(redirect_uri), false); - ub.append_query(U("code_verifier"), code_verifier, false); + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with private_key_jwt and code_verifier: " << utility::us2s(code_verifier) << " and client_assertion: " << utility::us2s(client_assertion); + auto ub = make_request_token_base_query(code, redirect_uri, code_verifier); // use private_key_jwt client authentication // see https://tools.ietf.org/html/rfc7523#section-2.2 ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); @@ -734,16 +733,28 @@ namespace nmos return request_token(client, version, ub, client_id, {}, scope, gate, token); } - // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant - pplx::task request_token_from_refresh_token(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& refresh_token, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + web::uri_builder make_request_token_base_query(const utility::string_t& refresh_token) { - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant"; - using web::http::oauth2::details::oauth2_strings; web::uri_builder ub; ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + return ub; + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant + pplx::task request_token_from_refresh_token(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& refresh_token, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant"; + + //using web::http::oauth2::details::oauth2_strings; + + //web::uri_builder ub; + //ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); + //ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + + auto ub = make_request_token_base_query(refresh_token); return request_token(client, version, ub, client_id, client_secret, scope, gate, token); } @@ -755,9 +766,7 @@ namespace nmos using web::http::oauth2::details::oauth2_strings; - web::uri_builder ub; - ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); - ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + auto ub = make_request_token_base_query(refresh_token); // use private_key_jwt client authentication // see https://tools.ietf.org/html/rfc7523#section-2.2 @@ -767,20 +776,49 @@ namespace nmos return request_token(client, version, ub, client_id, {}, scope, gate, token); } - // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication // this function is based on the oauth2_config::token_from_redirected_uri - pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token) + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token) { using web::http::oauth2::experimental::oauth2_exception; using web::http::oauth2::details::oauth2_strings; namespace response_types = web::http::oauth2::experimental::response_types; - // authorization code grant - // redirected_uri: /x-authorization/callback/?state=&code= - // implicit grant - // redirected_uri: /x-authorization/callback/#state=&code= or - // redirected_uri: /x-authorization/callback/#state=&access_token= - auto query = web::uri::split_query(response_type == response_types::token.name ? redirected_uri.fragment() : redirected_uri.query()); + std::map query; + + // for Authorization Code Grant Type Response (response_type = code) + // "If the resource owner grants the access request, the authorization + // server issues an authorization codeand delivers it to the client by + // adding the following parameters to the query component of the + // redirection URI using the "application/x-www-form-urlencoded" format + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response : + // HTTP / 1.1 302 Found + // Location : https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz + // see https://tools.ietf.org/html/rfc6749#section-4.1.2 + if (response_type == response_types::code.name) + { + query = web::uri::split_query(redirected_uri.query()); + } + // for Implicit Grant Type Response (response_type = token) + // "If the resource owner grants the access request, the authorization + // server issues an access tokenand delivers it to the client by adding + // the following parameters to the fragment component of the redirection + // URI using the "application/x-www-form-urlencoded" format" + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response + // HTTP / 1.1 302 Found + // Location : http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600 + else if (response_type == response_types::token.name) + { + query = web::uri::split_query(redirected_uri.fragment()); + } + else + { + throw oauth2_exception(U("response_type: '") + response_type + U("' is not supported")); + } auto state_param = query.find(oauth2_strings::state); if (state_param == query.end()) @@ -790,74 +828,50 @@ namespace nmos if (state != state_param->second) { - utility::string_t err(U("redirected URI parameter 'state'='")); - err += state_param->second; - err += U("' does not match excepted state='"); - err += state; - err += U("'"); - throw oauth2_exception(std::move(err)); - } - - auto code_param = query.find(oauth2_strings::code); - if (code_param != query.end()) - { - return request_token_from_authorization_code(client, version, client_id, client_secret, scope, code_param->second, redirect_uri, code_verifier, gate, token); + throw oauth2_exception(U("parameter 'state': '") + state_param->second + U("' does not match with the expected 'state': '") + state + U("'")); } - // NOTE: The redirected URI contains access token only in the implicit grant - // The implicit grant never passes a refresh token - auto token_param = query.find(oauth2_strings::access_token); - if (token_param == query.end()) + // for Authorization Code Grant Type Response (response_type = code) + // do request_token_from_authorization_code + if (response_type == response_types::code.name) { - throw oauth2_exception(U("either 'code' or 'access_token' parameter must be in the redirected URI")); - } - - return pplx::task_from_result(web::http::oauth2::experimental::oauth2_token(token_param->second)); - } - - // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication - // this function is based on the oauth2_config::token_from_redirected_uri - pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const utility::string_t& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token) - { - using web::http::oauth2::experimental::oauth2_exception; - using web::http::oauth2::details::oauth2_strings; - namespace response_types = web::http::oauth2::experimental::response_types; - - // authorization code grant - // redirected_uri: /auth/callback/?state=&code= - // implicit grant - // redirected_uri: /auth/callback/#state=&code= or - // redirected_uri: /auth/callback/#state=&access_token= - auto query = web::uri::split_query(response_type == response_types::token.name ? redirected_uri.fragment() : redirected_uri.query()); + auto code_param = query.find(oauth2_strings::code); + if (code_param == query.end()) + { + throw oauth2_exception(U("parameter 'code' missing from redirected URI")); + } - auto state_param = query.find(oauth2_strings::state); - if (state_param == query.end()) - { - throw oauth2_exception(U("parameter 'state' missing from redirected URI")); + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + return request_token_from_authorization_code_with_private_key_jwt(client, version, client_id, scope, code_param->second, redirect_uri, code_verifier, client_assertion, gate, token); + } + else if (web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic == token_endpoint_auth_method) + { + return request_token_from_authorization_code(client, version, client_id, client_secret, scope, code_param->second, redirect_uri, code_verifier, gate, token); + } + else + { + throw oauth2_exception(U("token_endpoint_auth_method: '") + token_endpoint_auth_method.name + U("' is not curently supported")); + } } - if (state != state_param->second) + // for Implicit Grant Type Response (response_type = token) + // extract access token from query parameters + auto token_type_param = query.find(oauth2_strings::token_type); + if (token_type_param == query.end()) { - utility::string_t err(U("redirected URI parameter 'state'='")); - err += state_param->second; - err += U("' does not match excepted state='"); - err += state; - err += U("'"); - throw oauth2_exception(std::move(err)); + throw oauth2_exception(U("parameter 'token_type' missing from redirected URI")); } - auto code_param = query.find(oauth2_strings::code); - if (code_param != query.end()) + if (boost::algorithm::to_lower_copy(token_type_param->second) != U("bearer")) { - return request_token_from_authorization_code(client, version, client_id, scope, code_param->second, redirect_uri, code_verifier, token_endpoint_auth_method, client_assertion, gate, token); + throw oauth2_exception(U("invalid parameter 'token_type': '") + token_type_param->second + U("', expecting 'bearer'")); } - // NOTE: The redirected URI contains access token only in the implicit grant - // The implicit grant never passes a refresh token auto token_param = query.find(oauth2_strings::access_token); if (token_param == query.end()) { - throw oauth2_exception(U("either 'code' or 'access_token' parameter must be in the redirected URI")); + throw oauth2_exception(U("parameter 'access_token' missing from redirected URI")); } return pplx::task_from_result(web::http::oauth2::experimental::oauth2_token(token_param->second)); @@ -897,7 +911,8 @@ namespace nmos const auto client_id = nmos::experimental::fields::client_id(client_metadata); const auto client_secret = client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""); const auto scope = nmos::experimental::fields::scope(client_metadata); - const auto token_endpoint_auth_method = client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method) ? nmos::experimental::fields::token_endpoint_auth_method(client_metadata) : web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name; + const auto token_endpoint_auth_method = client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method) ? + web::http::oauth2::experimental::to_token_endpoint_auth_method(nmos::experimental::fields::token_endpoint_auth_method(client_metadata)) : web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic; const auto token_endpoint = nmos::experimental::fields::token_endpoint(authorization_server_metadata); const auto client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(model.settings)); @@ -922,7 +937,7 @@ namespace nmos utility::string_t client_assertion; with_read_lock(model.mutex, [&] { - if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) { // use the 1st RSA private key from RSA private keys list to create the client_assertion if (!token_state.load_rsa_private_keys) @@ -940,7 +955,7 @@ namespace nmos if (web::http::oauth2::experimental::grant_types::authorization_code == token_state.grant_type) { - if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) { return request_token_from_refresh_token_using_private_key_jwt(*token_state.client, token_state.version, client_id, scope, token_state.bearer_token.refresh_token(), client_assertion, gate, token); } @@ -951,7 +966,7 @@ namespace nmos } else if (web::http::oauth2::experimental::grant_types::client_credentials == token_state.grant_type) { - if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) { return request_token_from_client_credentials_using_private_key_jwt(*token_state.client, token_state.version, client_id, scope, client_assertion, gate, token); } @@ -1334,8 +1349,10 @@ namespace nmos { auto lock = model.write_lock(); - if ((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::none.name) - && (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name) + // check client_secret existence for confidential client + if (((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_post.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_jwt.name)) && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) { slog::log(gate, SLOG_FLF) << "Missing client_secret"; @@ -1489,8 +1506,9 @@ namespace nmos // check client_secret existence for confidential client if (client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method)) { - if ((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::none.name) - && (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) != web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name) + if (((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_post.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_jwt.name)) && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) { slog::log(gate, SLOG_FLF) << "Missing client_secret"; @@ -1499,8 +1517,9 @@ namespace nmos } else { - if ((web::http::oauth2::experimental::token_endpoint_auth_methods::none != token_endpoint_auth_method) - && (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt != token_endpoint_auth_method) + if (((web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic == token_endpoint_auth_method) + || (web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_post == token_endpoint_auth_method) + || (web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_jwt == token_endpoint_auth_method)) && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) { slog::log(gate, SLOG_FLF) << "Missing client_secret"; @@ -1632,7 +1651,7 @@ namespace nmos request_authorization_code(make_authorization_code_uri(authorization_endpoint, client_id, redirct_uri, web::http::oauth2::experimental::response_types::code, scopes, code_challenge_methods_supported, authorization_state.state, authorization_state.code_verifier)); }); - // wait until received access token, or timeout + // wait for the access token const auto& authorization_code_flow_max = nmos::experimental::fields::authorization_code_flow_max(settings); if (authorization_code_flow_max > -1) { diff --git a/Development/nmos/authorization_operation.h b/Development/nmos/authorization_operation.h index c3d8b10eb..f3f95de04 100644 --- a/Development/nmos/authorization_operation.h +++ b/Development/nmos/authorization_operation.h @@ -8,6 +8,20 @@ namespace slog class base_gate; } +namespace web +{ + namespace http + { + namespace oauth2 + { + namespace experimental + { + struct token_endpoint_auth_method; + } + } + } +} + namespace nmos { struct base_model; @@ -28,11 +42,7 @@ namespace nmos // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token // this function is based on the oauth2::token_from_redirected_uri - pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); - - // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication - // this function is based on the oauth2::token_from_redirected_uri - pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const utility::string_t& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); // make an asynchronously GET request on the Authorization API to fetch authorization server metadata bool request_authorization_server_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, bool& authorization_service_error, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp index 27b17637f..295681f3e 100644 --- a/Development/nmos/authorization_redirect_api.cpp +++ b/Development/nmos/authorization_redirect_api.cpp @@ -1,5 +1,6 @@ #include "nmos/authorization_redirect_api.h" +#include "cpprest/access_token_error.h" #include "cpprest/response_type.h" #include "nmos/api_utils.h" #include "nmos/authorization_behaviour.h" // for top_authorization_service @@ -17,6 +18,21 @@ namespace nmos { namespace experimental { + struct authorization_flow_exception : std::runtime_error + { + web::http::oauth2::experimental::access_token_error error; + utility::string_t description; + + explicit authorization_flow_exception(web::http::oauth2::experimental::access_token_error error) + : std::runtime_error(utility::us2s(error.name)) + , error(std::move(error)) {} + + explicit authorization_flow_exception(web::http::oauth2::experimental::access_token_error error, utility::string_t description) + : std::runtime_error(utility::us2s(error.name)) + , error(std::move(error)) + , description(std::move(description)) {} + }; + namespace details { typedef std::pair authorization_flow_response; @@ -30,6 +46,88 @@ namespace nmos { return make_authorization_flow_error_response(code, {}, utility::s2us(debug.what())); } + + void process_error_response(const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& state) + { + using web::http::oauth2::experimental::oauth2_exception; + using web::http::oauth2::details::oauth2_strings; + namespace response_types = web::http::oauth2::experimental::response_types; + namespace access_token_errors = web::http::oauth2::experimental::access_token_errors; + + std::map query; + + // for Authorization Code Grant + // "If the resource owner denies the access request or if the request + // fails for reasons other than a missing or invalid redirection URI, + // the authorization server informs the client by adding the following + // parameters to the query component of the redirection URI using the + // "application/x-www-form-urlencoded" format" + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response + // HTTP/1.1 302 Found + // Location: https://client.example.com/cb?error=access_denied&state=xyz + // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + if (response_type == response_types::code.name) + { + query = web::uri::split_query(redirected_uri.query()); + } + // for Implicit Grant + // "If the resource owner denies the access request or if the request + // fails for reasons other than a missing or invalid redirection URI, + // the authorization server informs the client by adding the following + // parameters to the fragment component of the redirection URI using the + // "application/x-www-form-urlencoded" format" + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response + // HTTP/1.1 302 Found + // Location: https://client.example.com/cb#error=access_denied&state=xyz + // see https://tools.ietf.org/html/rfc6749#section-4.2.2.1 + else if (response_type == response_types::token.name) + { + query = web::uri::split_query(redirected_uri.fragment()); + } + else + { + throw oauth2_exception(U("response_type: '") + response_type + U("' is not supported")); + } + + auto state_param = query.find(oauth2_strings::state); + if (state_param == query.end()) + { + throw oauth2_exception(U("parameter 'state' missing from redirected URI")); + } + + if (state != state_param->second) + { + throw oauth2_exception(U("parameter 'state': '") + state_param->second + U("' does not match with the expected 'state': '") + state + U("'")); + } + + auto error_param = query.find(U("error")); + if (error_param != query.end()) + { + const auto error = web::http::oauth2::experimental::to_access_token_error(error_param->second); + if (error.empty()) + { + throw oauth2_exception(U("invalid 'error' parameter")); + } + + auto error_description_param = query.find(U("error_description")); + if (error_description_param != query.end()) + { + auto error_description = web::uri::decode(error_description_param->second); + std::replace(error_description.begin(), error_description.end(), '+', ' '); + throw authorization_flow_exception(error, error_description); + } + else + { + throw authorization_flow_exception(error); + } + + // hmm, error_uri is ignored for now + } + } } web::http::experimental::listener::api_router make_authorization_redirect_api(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate_) @@ -107,25 +205,79 @@ namespace nmos client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings)); }); + // The Authorization server may redirect error back due to something have went wrong + // such as resource owner rejects the request or the developer did something wrong + // when creating the Authorization request + { + auto lock = authorization_state.write_lock(); // in order to update shared state + try + { + details::process_error_response(req.request_uri(), response_type, state); + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request OAuth 2.0 error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const authorization_flow_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request Authorization Flow error: " << utility::us2s(e.error.name) << " description: " << utility::us2s(e.description); + result = details::make_authorization_flow_error_response(status_codes::BadRequest, e.error.name, e.description); + authorization_state.authorization_flow = authorization_state::failed; + } + + if (authorization_state::failed == authorization_state.authorization_flow) + { + with_write_lock(model.mutex, [&] + { + model.notify(); + }); + + set_reply(res, result.first, !result.second.is_null() ? result.second : nmos::make_error_response_body(result.first)); + + return pplx::task_from_result(true); + } + } + web::http::client::http_client client(token_endpoint, config); auto request_token = pplx::task_from_result(web::http::oauth2::experimental::oauth2_token()); - if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == token_endpoint_auth_method) - { - const auto client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key, keyid); + const auto token_endpoint_auth_meth = web::http::oauth2::experimental::to_token_endpoint_auth_method(token_endpoint_auth_method); - // exchange authorization code for bearer token - // where redirected URI: /x-authorization/callback/?state=&code= - request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, scope, redirect_uri, state, code_verifier, token_endpoint_auth_method, client_assertion, gate); - } - else + // create client assertion for private_key_jwt + utility::string_t client_assertion; + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_meth) { - // exchange authorization code for bearer token - // where redirected URI: /x-authorization/callback/?state=&code= - request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, client_secret, scope, redirect_uri, state, code_verifier, gate); + auto lock = authorization_state.write_lock(); // in order to update shared state + try + { + client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key, keyid); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request Create Client Assertion error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + + if (authorization_state::failed == authorization_state.authorization_flow) + { + with_write_lock(model.mutex, [&] + { + model.notify(); + }); + + set_reply(res, result.first, !result.second.is_null() ? result.second : nmos::make_error_response_body(result.first)); + + return pplx::task_from_result(true); + } } + // exchange authorization code for bearer token + request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, client_secret, scope, redirect_uri, state, code_verifier, token_endpoint_auth_meth, client_assertion, gate); + auto request = request_token.then([&model, &authorization_state, &scope, &gate](web::http::oauth2::experimental::oauth2_token bearer_token) { auto lock = authorization_state.write_lock(); @@ -145,7 +297,7 @@ namespace nmos try { finally.get(); - result = { status_codes::OK, web::json::value_of({U("Bearer token received")}) }; + result = { status_codes::OK, web::json::value_of({{ U("status"), U("Bearer token received") }}, true) }; } catch (const web::http::http_exception& e) { @@ -165,6 +317,12 @@ namespace nmos result = details::make_authorization_flow_error_response(status_codes::InternalError, e); authorization_state.authorization_flow = authorization_state::failed; } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } catch (...) { slog::log(gate, SLOG_FLF) << "Authorization flow token request error"; diff --git a/Development/nmos/authorization_utils.cpp b/Development/nmos/authorization_utils.cpp index add0d15e0..680c152c4 100644 --- a/Development/nmos/authorization_utils.cpp +++ b/Development/nmos/authorization_utils.cpp @@ -17,7 +17,7 @@ namespace nmos // get grant type set from json array std::set grant_types(const web::json::array& grants) { - return boost::copy_range>(grants | boost::adaptors::transformed([](const web::json::value& v) { return web::http::oauth2::experimental::parse_grant(v.as_string()); })); + return boost::copy_range>(grants | boost::adaptors::transformed([](const web::json::value& v) { return web::http::oauth2::experimental::to_grant_type(v.as_string()); })); } // get grant type set from settings @@ -58,7 +58,7 @@ namespace nmos // get token_endpoint_auth_method from settings web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method_from_settings(const nmos::settings& settings) { - return web::http::oauth2::experimental::parse_token_endpoint_auth_method(nmos::experimental::fields::token_endpoint_auth_method(settings)); + return web::http::oauth2::experimental::to_token_endpoint_auth_method(nmos::experimental::fields::token_endpoint_auth_method(settings)); } // get token_endpoint_auth_method from client metadata if presented, otherwise return default_token_endpoint_auth_method diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index c88045ef2..2a5bb89ae 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -144,6 +144,7 @@ namespace nmos const web::uri authapi_jwks_response_schema_uri = make_schema_uri(tag, _XPLATSTR("jwks_response.json")); const web::uri authapi_register_client_error_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_error_response.json")); const web::uri authapi_register_client_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_response.json")); + const web::uri authapi_token_error_response_uri = make_schema_uri(tag, _XPLATSTR("token_error_response.json")); const web::uri authapi_token_response_schema_uri = make_schema_uri(tag, _XPLATSTR("token_response.json")); const web::uri authapi_token_schema_schema_uri = make_schema_uri(tag, _XPLATSTR("token_schema.json")); } @@ -344,6 +345,7 @@ namespace nmos { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_schema.json")), make_schema(v1_0::jwks_schema) }, { make_schema_uri(v1_0::tag, _XPLATSTR("register_client_error_response.json")), make_schema(v1_0::register_client_error_response) }, { make_schema_uri(v1_0::tag, _XPLATSTR("register_client_response.json")), make_schema(v1_0::register_client_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("token_error_response.json")), make_schema(v1_0::token_error_response) }, { make_schema_uri(v1_0::tag, _XPLATSTR("token_response.json")), make_schema(v1_0::token_response) }, { make_schema_uri(v1_0::tag, _XPLATSTR("token_schema.json")), make_schema(v1_0::token_schema) } }; @@ -437,6 +439,11 @@ namespace nmos return is10_schemas::v1_0::authapi_register_client_response_uri; } + web::uri make_authapi_token_error_response_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_token_error_response_uri; + } + web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version) { return is10_schemas::v1_0::authapi_token_schema_schema_uri; diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index e74ae19cd..4c8c7b60a 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -32,6 +32,7 @@ namespace nmos web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version); web::uri make_authapi_jwks_response_schema_uri(const nmos::api_version& version); web::uri make_authapi_register_client_response_uri(const nmos::api_version& version); + web::uri make_authapi_token_error_response_uri(const nmos::api_version& version); web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version); web::uri make_authapi_token_response_schema_uri(const nmos::api_version& version); diff --git a/Development/nmos/jwks_uri_api.cpp b/Development/nmos/jwks_uri_api.cpp index cc44dc333..59733bc56 100644 --- a/Development/nmos/jwks_uri_api.cpp +++ b/Development/nmos/jwks_uri_api.cpp @@ -33,8 +33,6 @@ namespace nmos { using web::json::array; - // hmm, for now new "kid" key ID is used for every request, may be it should only be updated after key has changed - auto keys = value::array(); std::vector rsa_private_keys; with_read_lock(model.mutex, [&model, &rsa_private_keys, load_rsa_private_keys] diff --git a/Development/nmos/jwt_generator_impl.cpp b/Development/nmos/jwt_generator_impl.cpp index fc24c77d2..f063fd2c3 100644 --- a/Development/nmos/jwt_generator_impl.cpp +++ b/Development/nmos/jwt_generator_impl.cpp @@ -43,14 +43,7 @@ namespace nmos utility::string_t jwt_generator::create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid) { - try - { - return details::jwt_generator_impl::create_client_assertion(issuer, subject, audience, token_lifetime, private_key, keyid); - } - catch (const jwt::error::signature_generation_exception& e) - { - throw std::invalid_argument(e.what()); - } + return details::jwt_generator_impl::create_client_assertion(issuer, subject, audience, token_lifetime, private_key, keyid); } } } diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 0523012f0..5d96b31b6 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -412,7 +412,7 @@ namespace nmos const web::json::field_as_array authorization_scopes{ U("authorization_scopes") }; // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint - // supported methods are none, client_secret_basic and private_key_jwt, default to client_secret_basic, where none is used for public client + // supported methods are client_secret_basic and private_key_jwt, default to client_secret_basic const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic")}; // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration From 647851d2d538f6c37b8f20e391673d4536cb63f1 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 1 Mar 2023 22:16:35 +0000 Subject: [PATCH 007/121] lock before accessing authorization_state.authorization_flow --- Development/nmos/authorization_handlers.cpp | 8 ++--- Development/nmos/authorization_operation.cpp | 38 ++++++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index ebbf56892..628d73883 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -199,10 +199,10 @@ namespace nmos #else browser_cmd = "xdg-open \"" + utility::us2s(authorization_code_uri.to_string()) + "\""; #endif - if (0 > system(browser_cmd.c_str())) - { - slog::log(gate, SLOG_FLF) << "Failed to open a browser to start the authorization code flow"; - } + system(browser_cmd.c_str()); + + // TODO: process Authorization Server error response + // notify authorization_code_flow in the authorization_behaviour thread }; } diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index aaf67daee..b26e98beb 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -1641,6 +1641,9 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Attempting authorization code flow for scope: '" << nmos::experimental::details::make_scope(scopes) << "'"; + auto access_token_received = false; + auto authorization_flow = nmos::experimental::authorization_state::request_code; + // start the authorization code grant workflow, the authorization URI is required to // be loaded in the web browser to kick start the authorization code grant workflow if (request_authorization_code) @@ -1656,17 +1659,46 @@ namespace nmos if (authorization_code_flow_max > -1) { // wait access token with timeout - if (!model.wait_for(lock, std::chrono::seconds(authorization_code_flow_max), [&] { return shutdown || nmos::experimental::authorization_state::failed == authorization_state.authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_state.authorization_flow; })) + if (!model.wait_for(lock, std::chrono::seconds(authorization_code_flow_max), [&] { + authorization_flow = with_read_lock(authorization_state.mutex, [&] { return authorization_state.authorization_flow; }); + return shutdown || nmos::experimental::authorization_state::failed == authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_flow; })) { // authorization code workflow timeout authorization_service_error = true; slog::log(gate, SLOG_FLF) << "authorization code workflow timeout"; } + else if (nmos::experimental::authorization_state::access_token_received == authorization_flow) + { + // access token received + access_token_received = true; + slog::log(gate, SLOG_FLF) << "access token received"; + } + else + { + // authorization code workflow failure + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "authorization code workflow failure"; + } } else { // wait access token without timeout - condition.wait(lock, [&] { return shutdown || nmos::experimental::authorization_state::failed == authorization_state.authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_state.authorization_flow; }); + condition.wait(lock, [&] { + authorization_flow = with_read_lock(authorization_state.mutex, [&] { return authorization_state.authorization_flow; }); + return shutdown || nmos::experimental::authorization_state::failed == authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_flow; }); + + if (nmos::experimental::authorization_state::access_token_received == authorization_flow) + { + // access token received + access_token_received = true; + slog::log(gate, SLOG_FLF) << "access token received"; + } + else + { + // authorization code workflow failure + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "authorization code workflow failure"; + } } } else @@ -1678,7 +1710,7 @@ namespace nmos model.notify(); - return !authorization_service_error && nmos::experimental::authorization_state::access_token_received == authorization_state.authorization_flow; + return !authorization_service_error && access_token_received; } // fetch the bearer access token for the required scope(s) to access the protected APIs From 985c11dd1ee41eca1bbfc4bbc8274d02edd3f296 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 1 Mar 2023 22:17:20 +0000 Subject: [PATCH 008/121] different port maybe used for the redirect_uri and the jwks_uri --- Development/nmos/authorization_redirect_api.cpp | 2 +- Development/nmos/jwks_uri_api.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp index 295681f3e..c79d0b40a 100644 --- a/Development/nmos/authorization_redirect_api.cpp +++ b/Development/nmos/authorization_redirect_api.cpp @@ -144,7 +144,7 @@ namespace nmos authorization_api.support(U("/x-authorization/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) { - set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("callback/"), U("jwks/")}, req, res)); + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("callback/") }, req, res)); return pplx::task_from_result(true); }); diff --git a/Development/nmos/jwks_uri_api.cpp b/Development/nmos/jwks_uri_api.cpp index 59733bc56..2f8e408f7 100644 --- a/Development/nmos/jwks_uri_api.cpp +++ b/Development/nmos/jwks_uri_api.cpp @@ -25,7 +25,7 @@ namespace nmos jwks_api.support(U("/x-authorization/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) { - set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("callback/"), U("jwks/") }, req, res)); + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("jwks/") }, req, res)); return pplx::task_from_result(true); }); From f6759ee7ed068bace1b9fd02fc9d5d8269ecfbf8 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 1 Mar 2023 22:18:02 +0000 Subject: [PATCH 009/121] the redirect_uri and the jwks_uri are default using the same port --- Development/nmos-cpp-node/config.json | 2 +- Development/nmos/settings.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 2b1f6fc33..b54f37e36 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -327,7 +327,7 @@ //"token_endpoint_auth_method": "client_secret_basic", // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration - //"jwks_uri_port": 3219, + //"jwks_uri_port": 3218, // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) //"validate_openid_client": true, diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 5d96b31b6..e76c321dc 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -416,7 +416,7 @@ namespace nmos const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic")}; // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration - const web::json::field_as_integer_or jwks_uri_port{ U("jwks_uri_port"), 3219 }; + const web::json::field_as_integer_or jwks_uri_port{ U("jwks_uri_port"), 3218 }; // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) const web::json::field_as_bool_or validate_openid_client{ U("validate_openid_client"), true }; From 81aa45fa05ddaad94d989facfb1f3fdd1b6a6d45 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 2 Mar 2023 10:54:38 +0000 Subject: [PATCH 010/121] use conan 1.59.0 rather than using the latest version 2.0.0 --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/build-setup.yml | 2 +- Documents/Dependencies.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1332081cb..c09f3a195 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -93,7 +93,7 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan + pip install conan==1.59.0 conan config set general.revisions_enabled=1 - name: install cmake @@ -600,7 +600,7 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan + pip install conan==1.59.0 conan config set general.revisions_enabled=1 - name: install cmake diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index 0580afcfc..ff8ad926d 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -1,7 +1,7 @@ - name: install conan if: matrix.use_conan == true run: | - pip install conan + pip install conan==1.59.0 conan config set general.revisions_enabled=1 - name: install cmake diff --git a/Documents/Dependencies.md b/Documents/Dependencies.md index 58940e7ce..67dea2ee5 100644 --- a/Documents/Dependencies.md +++ b/Documents/Dependencies.md @@ -54,10 +54,10 @@ By default nmos-cpp uses [Conan](https://conan.io) to download most of its depen 1. Install Python 3 if necessary Note: The Python scripts directory needs to be added to the `PATH`, so the Conan executable can be found -2. Install Conan using `pip install conan` +2. Install Conan using `pip install conan==1.59.0` Notes: - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install conan` - - Currently, Conan 1.47 or higher is required; version 1.53.0 (latest release at the time) has been tested + - Currently, Conan 1.47 or up to version 1.59.0 is required; version 1.59.0 has been tested - Conan evolves fairly quickly, so it's worth running `pip install --upgrade conan` regularly - By default [Conan assumes semver compatibility](https://docs.conan.io/en/1.42/creating_packages/define_abi_compatibility.html#versioning-schema). Boost and other C++ libraries do not meet this expectation and break ABI compatibility between e.g. minor versions. From 15d8cb450a9723e3fead41eafaafafa7750552af Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 2 Mar 2023 14:09:51 +0000 Subject: [PATCH 011/121] fix to support new AUTH test added to the latest nmos testsuite --- Sandbox/run_nmos_testing.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index a2fd75147..d5d2c6169 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -114,17 +114,17 @@ else echo "Running non-Auth tests" auth=false # 6 test cases per API under test - (( expected_disabled_IS_04_01+=6 )) - (( expected_disabled_IS_04_03+=6 )) - (( expected_disabled_IS_05_01+=6 )) - (( expected_disabled_IS_05_02+=12 )) - (( expected_disabled_IS_07_01+=6 )) - (( expected_disabled_IS_07_02+=18 )) - (( expected_disabled_IS_08_01+=6 )) - (( expected_disabled_IS_08_02+=12 )) + (( expected_disabled_IS_04_01+=7 )) + (( expected_disabled_IS_04_03+=7 )) + (( expected_disabled_IS_05_01+=7 )) + (( expected_disabled_IS_05_02+=14 )) + (( expected_disabled_IS_07_01+=7 )) + (( expected_disabled_IS_07_02+=21 )) + (( expected_disabled_IS_08_01+=7 )) + (( expected_disabled_IS_08_02+=14 )) # test_33, test_33_1 - (( expected_disabled_IS_04_02+=14 )) - (( expected_disabled_IS_09_01+=6 )) + (( expected_disabled_IS_04_02+=16 )) + (( expected_disabled_IS_09_01+=7 )) fi "${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params}}" > ${results_dir}/nodeoutput 2>&1 & From ab3aec101d4de5dfcc3dcfe114f790041d2a4d3c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 2 Mar 2023 19:59:17 +0000 Subject: [PATCH 012/121] Update comment --- Sandbox/run_nmos_testing.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index d5d2c6169..9e45dcc6a 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -113,7 +113,7 @@ if [[ "${config_auth}" == "True" ]]; then else echo "Running non-Auth tests" auth=false - # 6 test cases per API under test + # 7 test cases per API under test (( expected_disabled_IS_04_01+=7 )) (( expected_disabled_IS_04_03+=7 )) (( expected_disabled_IS_05_01+=7 )) From 32787cce34ce859f850c4646ea2fb5a99036d37c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 3 Mar 2023 09:49:57 +0000 Subject: [PATCH 013/121] Use the lastest Conan 1.x version --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/build-setup.yml | 2 +- Documents/Dependencies.md | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c09f3a195..29cb358ba 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -93,7 +93,7 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan==1.59.0 + pip install "conan<2" conan config set general.revisions_enabled=1 - name: install cmake @@ -600,7 +600,7 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan==1.59.0 + pip install "conan<2" conan config set general.revisions_enabled=1 - name: install cmake diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index ff8ad926d..f8bb80164 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -1,7 +1,7 @@ - name: install conan if: matrix.use_conan == true run: | - pip install conan==1.59.0 + pip install "conan<2" conan config set general.revisions_enabled=1 - name: install cmake diff --git a/Documents/Dependencies.md b/Documents/Dependencies.md index 67dea2ee5..66f051630 100644 --- a/Documents/Dependencies.md +++ b/Documents/Dependencies.md @@ -54,10 +54,10 @@ By default nmos-cpp uses [Conan](https://conan.io) to download most of its depen 1. Install Python 3 if necessary Note: The Python scripts directory needs to be added to the `PATH`, so the Conan executable can be found -2. Install Conan using `pip install conan==1.59.0` +2. Install Conan using `pip install "conan<2"` Notes: - - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install conan` - - Currently, Conan 1.47 or up to version 1.59.0 is required; version 1.59.0 has been tested + - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install "conan<2"` + - Currently, Conan 1.47 or lower than version 2 is required; version 1.59.0 has been tested - Conan evolves fairly quickly, so it's worth running `pip install --upgrade conan` regularly - By default [Conan assumes semver compatibility](https://docs.conan.io/en/1.42/creating_packages/define_abi_compatibility.html#versioning-schema). Boost and other C++ libraries do not meet this expectation and break ABI compatibility between e.g. minor versions. From 6dc7e2c0d82b88525004c7fdec67c6c223114908 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 28 Apr 2023 13:35:55 +0100 Subject: [PATCH 014/121] Remove spaces --- Development/nmos/client_utils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index f2576e2cd..707df9104 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -196,7 +196,7 @@ namespace nmos return config; } - + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy // with the remaining options defaulted, e.g. authorization request timeout web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) From b3bd3a02586267d6d55280282aa5ee388fb88526 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 28 Apr 2023 13:40:52 +0100 Subject: [PATCH 015/121] Use jwt-cpp v0.7.0-rc.0 when not using conan, until v0.7 is released for conan, 0.5.1 is continuously to be used for conan --- Development/jwt/nlohmann_traits.h | 10 + Development/nmos/authorization.cpp | 6 +- Development/nmos/jwt_validator_impl.cpp | 2 +- Development/third_party/jwt-cpp/base.h | 301 ++-- Development/third_party/jwt-cpp/jwt.h | 1985 +++++++++++++++-------- 5 files changed, 1471 insertions(+), 833 deletions(-) diff --git a/Development/jwt/nlohmann_traits.h b/Development/jwt/nlohmann_traits.h index a2817c6bb..f15b7710a 100644 --- a/Development/jwt/nlohmann_traits.h +++ b/Development/jwt/nlohmann_traits.h @@ -74,6 +74,11 @@ namespace jwt } } + static int64_t as_integer(const json &val) + { + return as_int(val); + } + static bool as_bool(const json &val) { if (val.type() != json::value_t::boolean) @@ -83,6 +88,11 @@ namespace jwt return val.get(); } + static bool as_boolean(const json &val) + { + return as_bool(val); + } + static double as_number(const json &val) { if (val.type() != json::value_t::number_float) diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp index e8dbace7d..f1fbcdd6c 100644 --- a/Development/nmos/authorization.cpp +++ b/Development/nmos/authorization.cpp @@ -81,7 +81,7 @@ namespace nmos slog::log(gate, SLOG_FLF) << "missing access token"; return{ authorization_error::without_authentication, "missing access token" }; } - + try { // extract the token issuer from the token @@ -96,7 +96,7 @@ namespace nmos #endif return{ authorization_error::failed, e.what() }; } - + // find the relevent issuer's public keys to validate the token std::string error; auto issuer = issuers.find(token_issuer); @@ -124,7 +124,7 @@ namespace nmos slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; #else slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; -#endif +#endif } catch (const insufficient_scope_exception& e) { diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index c65e73e13..7f8fbb659 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -65,7 +65,7 @@ namespace nmos validator.allow_algorithm(jwt::algorithm::rs512(utility::us2s(pubkey.at(U("pem")).as_string()))); validators.push_back(validator); } - catch (const jwt::rsa_exception&) + catch (const jwt::error::rsa_exception&) { // hmm, maybe log the error? } diff --git a/Development/third_party/jwt-cpp/base.h b/Development/third_party/jwt-cpp/base.h index c447113c9..cef493d19 100644 --- a/Development/third_party/jwt-cpp/base.h +++ b/Development/third_party/jwt-cpp/base.h @@ -1,9 +1,11 @@ #ifndef JWT_CPP_BASE_H #define JWT_CPP_BASE_H +#include #include #include #include +#include #ifdef __has_cpp_attribute #if __has_cpp_attribute(fallthrough) @@ -21,7 +23,10 @@ namespace jwt { */ namespace alphabet { /** - * \brief valid list of characted when working with [Base64](https://tools.ietf.org/html/rfc3548) + * \brief valid list of character when working with [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4) + * + * As directed in [X.509 Parameter](https://datatracker.ietf.org/doc/html/rfc7517#section-4.7) certificate chains are + * base64-encoded as per [Section 4 of RFC4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4) */ struct base64 { static const std::array& data() { @@ -38,7 +43,13 @@ namespace jwt { } }; /** - * \brief valid list of characted when working with [Base64URL](https://tools.ietf.org/html/rfc4648) + * \brief valid list of character when working with [Base64URL](https://tools.ietf.org/html/rfc4648#section-5) + * + * As directed by [RFC 7519 Terminology](https://datatracker.ietf.org/doc/html/rfc7519#section-2) set the definition of Base64URL + * encoding as that in [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-2) that states: + * + * > Base64 encoding using the URL- and filename-safe character set defined in + * > [Section 5 of RFC 4648 RFC4648](https://tools.ietf.org/html/rfc4648#section-5), with all trailing '=' characters omitted */ struct base64url { static const std::array& data() { @@ -54,155 +65,205 @@ namespace jwt { return fill; } }; + namespace helper { + /** + * @brief A General purpose base64url alphabet respecting the + * [URI Case Normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1) + * + * This is useful in situations outside of JWT encoding/decoding and is provided as a helper + */ + struct base64url_percent_encoding { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; + return data; + } + static const std::initializer_list& fill() { + static std::initializer_list fill{"%3D", "%3d"}; + return fill; + } + }; + } // namespace helper + + inline uint32_t index(const std::array& alphabet, char symbol) { + auto itr = std::find_if(alphabet.cbegin(), alphabet.cend(), [symbol](char c) { return c == symbol; }); + if (itr == alphabet.cend()) { throw std::runtime_error("Invalid input: not within alphabet"); } + + return std::distance(alphabet.cbegin(), itr); + } } // namespace alphabet /** - * \brief Alphabet generic methods for working with encoding/decoding the base64 family + * \brief A collection of fellable functions for working with base64 and base64url */ - class base { - public: - template - static std::string encode(const std::string& bin) { - return encode(bin, T::data(), T::fill()); - } - template - static std::string decode(const std::string& base) { - return decode(base, T::data(), T::fill()); - } - template - static std::string pad(const std::string& base) { - return pad(base, T::fill()); - } - template - static std::string trim(const std::string& base) { - return trim(base, T::fill()); - } + namespace base { - private: - static std::string encode(const std::string& bin, const std::array& alphabet, - const std::string& fill) { - size_t size = bin.size(); - std::string res; + namespace details { + struct padding { + size_t count = 0; + size_t length = 0; - // clear incomplete bytes - size_t fast_size = size - size % 3; - for (size_t i = 0; i < fast_size;) { - uint32_t octet_a = static_cast(bin[i++]); - uint32_t octet_b = static_cast(bin[i++]); - uint32_t octet_c = static_cast(bin[i++]); + padding() = default; + padding(size_t count, size_t length) : count(count), length(length) {} - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + padding operator+(const padding& p) { return padding(count + p.count, length + p.length); } - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += alphabet[(triple >> 1 * 6) & 0x3F]; - res += alphabet[(triple >> 0 * 6) & 0x3F]; - } + friend bool operator==(const padding& lhs, const padding& rhs) { + return lhs.count == rhs.count && lhs.length == rhs.length; + } + }; + + inline padding count_padding(const std::string& base, const std::vector& fills) { + for (const auto& fill : fills) { + if (base.size() < fill.size()) continue; + // Does the end of the input exactly match the fill pattern? + if (base.substr(base.size() - fill.size()) == fill) { + return padding{1, fill.length()} + + count_padding(base.substr(0, base.size() - fill.size()), fills); + } + } - if (fast_size == size) return res; - - size_t mod = size % 3; - - uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; - uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; - uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; - - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; - - switch (mod) { - case 1: - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += fill; - res += fill; - break; - case 2: - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += alphabet[(triple >> 1 * 6) & 0x3F]; - res += fill; - break; - default: break; + return {}; } - return res; - } + inline std::string encode(const std::string& bin, const std::array& alphabet, + const std::string& fill) { + size_t size = bin.size(); + std::string res; + + // clear incomplete bytes + size_t fast_size = size - size % 3; + for (size_t i = 0; i < fast_size;) { + uint32_t octet_a = static_cast(bin[i++]); + uint32_t octet_b = static_cast(bin[i++]); + uint32_t octet_c = static_cast(bin[i++]); + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += alphabet[(triple >> 0 * 6) & 0x3F]; + } + + if (fast_size == size) return res; - static std::string decode(const std::string& base, const std::array& alphabet, - const std::string& fill) { - size_t size = base.size(); - - size_t fill_cnt = 0; - while (size > fill.size()) { - if (base.substr(size - fill.size(), fill.size()) == fill) { - fill_cnt++; - size -= fill.size(); - if (fill_cnt > 2) throw std::runtime_error("Invalid input"); - } else + size_t mod = size % 3; + + uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + switch (mod) { + case 1: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += fill; + res += fill; break; + case 2: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += fill; + break; + default: break; + } + + return res; } - if ((size + fill_cnt) % 4 != 0) throw std::runtime_error("Invalid input"); + inline std::string decode(const std::string& base, const std::array& alphabet, + const std::vector& fill) { + const auto pad = count_padding(base, fill); + if (pad.count > 2) throw std::runtime_error("Invalid input: too much fill"); - size_t out_size = size / 4 * 3; - std::string res; - res.reserve(out_size); + const size_t size = base.size() - pad.length; + if ((size + pad.count) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size"); - auto get_sextet = [&](size_t offset) { - for (size_t i = 0; i < alphabet.size(); i++) { - if (alphabet[i] == base[offset]) return static_cast(i); + size_t out_size = size / 4 * 3; + std::string res; + res.reserve(out_size); + + auto get_sextet = [&](size_t offset) { return alphabet::index(alphabet, base[offset]); }; + + size_t fast_size = size - size % 4; + for (size_t i = 0; i < fast_size;) { + uint32_t sextet_a = get_sextet(i++); + uint32_t sextet_b = get_sextet(i++); + uint32_t sextet_c = get_sextet(i++); + uint32_t sextet_d = get_sextet(i++); + + uint32_t triple = + (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); + + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + res += static_cast((triple >> 0 * 8) & 0xFFU); } - throw std::runtime_error("Invalid input"); - }; - size_t fast_size = size - size % 4; - for (size_t i = 0; i < fast_size;) { - uint32_t sextet_a = get_sextet(i++); - uint32_t sextet_b = get_sextet(i++); - uint32_t sextet_c = get_sextet(i++); - uint32_t sextet_d = get_sextet(i++); + if (pad.count == 0) return res; + + uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); - uint32_t triple = (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); + switch (pad.count) { + case 1: + triple |= (get_sextet(fast_size + 2) << 1 * 6); + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + break; + case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; + default: break; + } - res += static_cast((triple >> 2 * 8) & 0xFFU); - res += static_cast((triple >> 1 * 8) & 0xFFU); - res += static_cast((triple >> 0 * 8) & 0xFFU); + return res; } - if (fill_cnt == 0) return res; + inline std::string decode(const std::string& base, const std::array& alphabet, + const std::string& fill) { + return decode(base, alphabet, std::vector{fill}); + } - uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); + inline std::string pad(const std::string& base, const std::string& fill) { + std::string padding; + switch (base.size() % 4) { + case 1: padding += fill; JWT_FALLTHROUGH; + case 2: padding += fill; JWT_FALLTHROUGH; + case 3: padding += fill; JWT_FALLTHROUGH; + default: break; + } - switch (fill_cnt) { - case 1: - triple |= (get_sextet(fast_size + 2) << 1 * 6); - res += static_cast((triple >> 2 * 8) & 0xFFU); - res += static_cast((triple >> 1 * 8) & 0xFFU); - break; - case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; - default: break; + return base + padding; } - return res; - } - - static std::string pad(const std::string& base, const std::string& fill) { - std::string padding; - switch (base.size() % 4) { - case 1: padding += fill; JWT_FALLTHROUGH; - case 2: padding += fill; JWT_FALLTHROUGH; - case 3: padding += fill; JWT_FALLTHROUGH; - default: break; + inline std::string trim(const std::string& base, const std::string& fill) { + auto pos = base.find(fill); + return base.substr(0, pos); } + } // namespace details - return base + padding; + template + std::string encode(const std::string& bin) { + return details::encode(bin, T::data(), T::fill()); } - - static std::string trim(const std::string& base, const std::string& fill) { - auto pos = base.find(fill); - return base.substr(0, pos); + template + std::string decode(const std::string& base) { + return details::decode(base, T::data(), T::fill()); + } + template + std::string pad(const std::string& base) { + return details::pad(base, T::fill()); + } + template + std::string trim(const std::string& base) { + return details::trim(base, T::fill()); } - }; + } // namespace base } // namespace jwt #endif diff --git a/Development/third_party/jwt-cpp/jwt.h b/Development/third_party/jwt-cpp/jwt.h index edd08912b..ec63b64c7 100644 --- a/Development/third_party/jwt-cpp/jwt.h +++ b/Development/third_party/jwt-cpp/jwt.h @@ -13,19 +13,31 @@ #endif #include +#include #include #include #include #include +#include +#include #include #include +#include +#include +#include +#include #include #include #include #include #include #include +#include + +#if __cplusplus > 201103L +#include +#endif #if __cplusplus >= 201402L #ifdef __has_include @@ -35,19 +47,26 @@ #endif #endif -// If openssl version less than 1.1 -#if OPENSSL_VERSION_NUMBER < 0x10100000L -#define OPENSSL10 +#if OPENSSL_VERSION_NUMBER >= 0x30000000L // 3.0.0 +#define JWT_OPENSSL_3_0 +#elif OPENSSL_VERSION_NUMBER >= 0x10101000L // 1.1.1 +#define JWT_OPENSSL_1_1_1 +#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // 1.1.0 +#define JWT_OPENSSL_1_1_0 +#elif OPENSSL_VERSION_NUMBER >= 0x10000000L // 1.0.0 +#define JWT_OPENSSL_1_0_0 #endif -// If openssl version less than 1.1.1 -#if OPENSSL_VERSION_NUMBER < 0x10101000L -#define OPENSSL110 +#if defined(LIBRESSL_VERSION_NUMBER) +#if LIBRESSL_VERSION_NUMBER >= 0x3050300fL +#define JWT_OPENSSL_1_1_0 +#else +#define JWT_OPENSSL_1_0_0 +#endif #endif -#if defined(LIBRESSL_VERSION_NUMBER) -#define OPENSSL10 -#define OPENSSL110 +#if defined(LIBWOLFSSL_VERSION_HEX) +#define JWT_OPENSSL_1_1_1 #endif #ifndef JWT_CLAIM_EXPLICIT @@ -62,6 +81,9 @@ * JWS (JSON Web Signature) from [RFC7515](https://tools.ietf.org/html/rfc7515) */ namespace jwt { + /** + * Default system time point in UTC + */ using date = std::chrono::system_clock::time_point; /** @@ -136,7 +158,8 @@ namespace jwt { create_mem_bio_failed, no_key_provided, invalid_key_size, - invalid_key + invalid_key, + create_context_failed }; /** * \brief Error category for ECDSA errors @@ -155,6 +178,7 @@ namespace jwt { return "at least one of public or private key need to be present"; case ecdsa_error::invalid_key_size: return "invalid key size"; case ecdsa_error::invalid_key: return "invalid key"; + case ecdsa_error::create_context_failed: return "failed to create context"; default: return "unknown ECDSA error"; } } @@ -175,7 +199,9 @@ namespace jwt { verifyinit_failed, verifyupdate_failed, verifyfinal_failed, - get_key_failed + get_key_failed, + set_rsa_pss_saltlen_failed, + signature_encoding_failed }; /** * \brief Error category for verification errors @@ -198,6 +224,10 @@ namespace jwt { return "failed to verify signature: VerifyFinal failed"; case signature_verification_error::get_key_failed: return "failed to verify signature: Could not get key"; + case signature_verification_error::set_rsa_pss_saltlen_failed: + return "failed to verify signature: EVP_PKEY_CTX_set_rsa_pss_saltlen failed"; + case signature_verification_error::signature_encoding_failed: + return "failed to verify signature: i2d_ECDSA_SIG failed"; default: return "unknown signature verification error"; } } @@ -226,7 +256,9 @@ namespace jwt { digestfinal_failed, rsa_padding_failed, rsa_private_encrypt_failed, - get_key_failed + get_key_failed, + set_rsa_pss_saltlen_failed, + signature_decoding_failed }; /** * \brief Error category for signature generation errors @@ -255,11 +287,15 @@ namespace jwt { case signature_generation_error::digestfinal_failed: return "failed to create signature: DigestFinal failed"; case signature_generation_error::rsa_padding_failed: - return "failed to create signature: RSA_padding_add_PKCS1_PSS_mgf1 failed"; + return "failed to create signature: EVP_PKEY_CTX_set_rsa_padding failed"; case signature_generation_error::rsa_private_encrypt_failed: return "failed to create signature: RSA_private_encrypt failed"; case signature_generation_error::get_key_failed: return "failed to generate signature: Could not get key"; + case signature_generation_error::set_rsa_pss_saltlen_failed: + return "failed to create signature: EVP_PKEY_CTX_set_rsa_pss_saltlen failed"; + case signature_generation_error::signature_decoding_failed: + return "failed to create signature: d2i_ECDSA_SIG failed"; default: return "unknown signature generation error"; } } @@ -326,15 +362,8 @@ namespace jwt { } } } // namespace error - - // FIXME: Remove - // Keep backward compat at least for a couple of revisions - using error::ecdsa_exception; - using error::rsa_exception; - using error::signature_generation_exception; - using error::signature_verification_exception; - using error::token_verification_exception; } // namespace jwt + namespace std { template<> struct is_error_code_enum : true_type {}; @@ -347,6 +376,7 @@ namespace std { template<> struct is_error_code_enum : true_type {}; } // namespace std + namespace jwt { /** * \brief A collection for working with certificates @@ -356,24 +386,119 @@ namespace jwt { * you maybe need to extract the modulus and exponent of an RSA Public Key. */ namespace helper { + /** + * \brief Handle class for EVP_PKEY structures + * + * Starting from OpenSSL 1.1.0, EVP_PKEY has internal reference counting. This handle class allows + * jwt-cpp to leverage that and thus safe an allocation for the control block in std::shared_ptr. + * The handle uses shared_ptr as a fallback on older versions. The behaviour should be identical between both. + */ + class evp_pkey_handle { + public: + constexpr evp_pkey_handle() noexcept = default; +#ifdef JWT_OPENSSL_1_0_0 + /** + * \brief Construct a new handle. The handle takes ownership of the key. + * \param key The key to store + */ + explicit evp_pkey_handle(EVP_PKEY* key) { m_key = std::shared_ptr(key, EVP_PKEY_free); } + + EVP_PKEY* get() const noexcept { return m_key.get(); } + bool operator!() const noexcept { return m_key == nullptr; } + explicit operator bool() const noexcept { return m_key != nullptr; } + + private: + std::shared_ptr m_key{nullptr}; +#else + /** + * \brief Construct a new handle. The handle takes ownership of the key. + * \param key The key to store + */ + explicit constexpr evp_pkey_handle(EVP_PKEY* key) noexcept : m_key{key} {} + evp_pkey_handle(const evp_pkey_handle& other) : m_key{other.m_key} { + if (m_key != nullptr && EVP_PKEY_up_ref(m_key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); + } +// C++11 requires the body of a constexpr constructor to be empty +#if __cplusplus >= 201402L + constexpr +#endif + evp_pkey_handle(evp_pkey_handle&& other) noexcept + : m_key{other.m_key} { + other.m_key = nullptr; + } + evp_pkey_handle& operator=(const evp_pkey_handle& other) { + if (&other == this) return *this; + decrement_ref_count(m_key); + m_key = other.m_key; + increment_ref_count(m_key); + return *this; + } + evp_pkey_handle& operator=(evp_pkey_handle&& other) noexcept { + if (&other == this) return *this; + decrement_ref_count(m_key); + m_key = other.m_key; + other.m_key = nullptr; + return *this; + } + evp_pkey_handle& operator=(EVP_PKEY* key) { + decrement_ref_count(m_key); + m_key = key; + increment_ref_count(m_key); + return *this; + } + ~evp_pkey_handle() noexcept { decrement_ref_count(m_key); } + + EVP_PKEY* get() const noexcept { return m_key; } + bool operator!() const noexcept { return m_key == nullptr; } + explicit operator bool() const noexcept { return m_key != nullptr; } + + private: + EVP_PKEY* m_key{nullptr}; + + static void increment_ref_count(EVP_PKEY* key) { + if (key != nullptr && EVP_PKEY_up_ref(key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); + } + static void decrement_ref_count(EVP_PKEY* key) noexcept { + if (key != nullptr) EVP_PKEY_free(key); + } +#endif + }; + + inline std::unique_ptr make_mem_buf_bio() { + return std::unique_ptr(BIO_new(BIO_s_mem()), BIO_free_all); + } + + inline std::unique_ptr make_mem_buf_bio(const std::string& data) { + return std::unique_ptr( +#if OPENSSL_VERSION_NUMBER <= 0x10100003L + BIO_new_mem_buf(const_cast(data.data()), static_cast(data.size())), BIO_free_all +#else + BIO_new_mem_buf(data.data(), static_cast(data.size())), BIO_free_all +#endif + ); + } + + inline std::unique_ptr make_evp_md_ctx() { + return +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr(EVP_MD_CTX_create(), &EVP_MD_CTX_destroy); +#else + std::unique_ptr(EVP_MD_CTX_new(), &EVP_MD_CTX_free); +#endif + } + /** * \brief Extract the public key of a pem certificate * * \param certstr String containing the certificate encoded as pem * \param pw Password used to decrypt certificate (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error ocurred) */ inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw, std::error_code& ec) { ec.clear(); -#if OPENSSL_VERSION_NUMBER <= 0x10100003L - std::unique_ptr certbio( - BIO_new_mem_buf(const_cast(certstr.data()), static_cast(certstr.size())), BIO_free_all); -#else - std::unique_ptr certbio( - BIO_new_mem_buf(certstr.data(), static_cast(certstr.size())), BIO_free_all); -#endif - std::unique_ptr keybio(BIO_new(BIO_s_mem()), BIO_free_all); + auto certbio = make_mem_buf_bio(certstr); + auto keybio = make_mem_buf_bio(); if (!certbio || !keybio) { ec = error::rsa_error::create_mem_bio_failed; return {}; @@ -440,7 +565,7 @@ namespace jwt { std::unique_ptr cert( d2i_X509(NULL, &c_str, static_cast(decodedStr.size())), X509_free); - std::unique_ptr certbio(BIO_new(BIO_s_mem()), BIO_free_all); + auto certbio = make_mem_buf_bio(); if (!cert || !certbio) { ec = error::rsa_error::create_mem_bio_failed; return {}; @@ -520,42 +645,38 @@ namespace jwt { * * The string should contain a pem encoded certificate or public key * - * \param certstr String containing the certificate encoded as pem - * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \param key String containing the certificate encoded as pem + * \param password Password used to decrypt certificate (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline std::shared_ptr load_public_key_from_string(const std::string& key, - const std::string& password, std::error_code& ec) { + inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { ec.clear(); - std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + auto pubkey_bio = make_mem_buf_bio(); if (!pubkey_bio) { ec = error::rsa_error::create_mem_bio_failed; - return nullptr; + return {}; } if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { auto epkey = helper::extract_pubkey_from_cert(key, password, ec); - if (ec) return nullptr; + if (ec) return {}; const int len = static_cast(epkey.size()); if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return nullptr; + return {}; } } else { const int len = static_cast(key.size()); if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return nullptr; + return {}; } } - std::shared_ptr pkey( - PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, - (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` - EVP_PKEY_free); - if (!pkey) { - ec = error::rsa_error::load_key_bio_read; - return nullptr; - } + evp_pkey_handle pkey(PEM_read_bio_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` + if (!pkey) ec = error::rsa_error::load_key_bio_read; return pkey; } @@ -564,12 +685,11 @@ namespace jwt { * * The string should contain a pem encoded certificate or public key * - * \param certstr String containing the certificate or key encoded as pem - * \param pw Password used to decrypt certificate or key (leave empty if not encrypted) + * \param key String containing the certificate or key encoded as pem + * \param password Password used to decrypt certificate or key (leave empty if not encrypted) * \throw rsa_exception if an error occurred */ - inline std::shared_ptr load_public_key_from_string(const std::string& key, - const std::string& password = "") { + inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password = "") { std::error_code ec; auto res = load_public_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -580,28 +700,24 @@ namespace jwt { * \brief Load a private key from a string. * * \param key String containing a private key as pem - * \param pw Password used to decrypt key (leave empty if not encrypted) + * \param password Password used to decrypt key (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline std::shared_ptr - load_private_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { - std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + auto privkey_bio = make_mem_buf_bio(); if (!privkey_bio) { ec = error::rsa_error::create_mem_bio_failed; - return nullptr; + return {}; } const int len = static_cast(key.size()); if (BIO_write(privkey_bio.get(), key.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return nullptr; - } - std::shared_ptr pkey( - PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), - EVP_PKEY_free); - if (!pkey) { - ec = error::rsa_error::load_key_bio_read; - return nullptr; + return {}; } + evp_pkey_handle pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); + if (!pkey) ec = error::rsa_error::load_key_bio_read; return pkey; } @@ -609,28 +725,124 @@ namespace jwt { * \brief Load a private key from a string. * * \param key String containing a private key as pem - * \param pw Password used to decrypt key (leave empty if not encrypted) + * \param password Password used to decrypt key (leave empty if not encrypted) * \throw rsa_exception if an error occurred */ - inline std::shared_ptr load_private_key_from_string(const std::string& key, - const std::string& password = "") { + inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password = "") { std::error_code ec; auto res = load_private_key_from_string(key, password, ec); error::throw_if_error(ec); return res; } + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param key String containing the certificate encoded as pem + * \param password Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + ec.clear(); + auto pubkey_bio = make_mem_buf_bio(); + if (!pubkey_bio) { + ec = error::ecdsa_error::create_mem_bio_failed; + return {}; + } + if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { + auto epkey = helper::extract_pubkey_from_cert(key, password, ec); + if (ec) return {}; + const int len = static_cast(epkey.size()); + if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return {}; + } + } else { + const int len = static_cast(key.size()); + if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return {}; + } + } + + evp_pkey_handle pkey(PEM_read_bio_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` + if (!pkey) ec = error::ecdsa_error::load_key_bio_read; + return pkey; + } + + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param key String containing the certificate or key encoded as pem + * \param password Password used to decrypt certificate or key (leave empty if not encrypted) + * \throw ecdsa_exception if an error occurred + */ + inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_public_ec_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param password Password used to decrypt key (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + auto privkey_bio = make_mem_buf_bio(); + if (!privkey_bio) { + ec = error::ecdsa_error::create_mem_bio_failed; + return {}; + } + const int len = static_cast(key.size()); + if (BIO_write(privkey_bio.get(), key.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return {}; + } + evp_pkey_handle pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); + if (!pkey) ec = error::ecdsa_error::load_key_bio_read; + return pkey; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param password Password used to decrypt key (leave empty if not encrypted) + * \throw ecdsa_exception if an error occurred + */ + inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_private_ec_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + /** * Convert a OpenSSL BIGNUM to a std::string * \param bn BIGNUM to convert * \return bignum as string */ inline -#ifdef OPENSSL10 - static std::string +#ifdef JWT_OPENSSL_1_0_0 + std::string bn2raw(BIGNUM* bn) #else - static std::string + std::string bn2raw(const BIGNUM* bn) #endif { @@ -643,7 +855,7 @@ namespace jwt { * \param raw String to convert * \return BIGNUM representation */ - inline static std::unique_ptr raw2bn(const std::string& raw) { + inline std::unique_ptr raw2bn(const std::string& raw) { return std::unique_ptr( BN_bin2bn(reinterpret_cast(raw.data()), static_cast(raw.size()), nullptr), BN_free); @@ -774,7 +986,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw rsa_exception(error::rsa_error::no_key_provided); + throw error::rsa_exception(error::rsa_error::no_key_provided); } /** * Sign jwt data @@ -784,11 +996,7 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); -#ifdef OPENSSL10 - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -821,11 +1029,7 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); -#ifdef OPENSSL10 - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -853,7 +1057,7 @@ namespace jwt { private: /// OpenSSL structure containing converted keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// Hash generator const EVP_MD* (*md)(); /// algorithm's name @@ -865,57 +1069,34 @@ namespace jwt { struct ecdsa { /** * Construct new ecdsa algorithm + * * \param public_key ECDSA public key in PEM format - * \param private_key ECDSA private key or empty string if not available. If empty, signing will always - * fail. \param public_key_password Password to decrypt public key pem. \param private_key_password Password - * to decrypt private key pem. \param md Pointer to hash function \param name Name of the algorithm + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always fail + * \param public_key_password Password to decrypt public key pem + * \param private_key_password Password to decrypt private key pem + * \param md Pointer to hash function + * \param name Name of the algorithm + * \param siglen The bit length of the signature */ ecdsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, const std::string& private_key_password, const EVP_MD* (*md)(), std::string name, size_t siglen) : md(md), alg_name(std::move(name)), signature_length(siglen) { - if (!public_key.empty()) { - std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); - if (!pubkey_bio) throw ecdsa_exception(error::ecdsa_error::create_mem_bio_failed); - if (public_key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { - auto epkey = helper::extract_pubkey_from_cert(public_key, public_key_password); - const int len = static_cast(epkey.size()); - if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) - throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); - } else { - const int len = static_cast(public_key.size()); - if (BIO_write(pubkey_bio.get(), public_key.data(), len) != len) - throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); - } - - pkey.reset(PEM_read_bio_EC_PUBKEY( - pubkey_bio.get(), nullptr, nullptr, - (void*)public_key_password - .c_str()), // NOLINT(google-readability-casting) requires `const_cast` - EC_KEY_free); - if (!pkey) throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); - size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); - if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) - throw ecdsa_exception(error::ecdsa_error::invalid_key_size); + if (!private_key.empty()) { + pkey = helper::load_private_ec_key_from_string(private_key, private_key_password); + check_private_key(pkey.get()); + } else if (!public_key.empty()) { + pkey = helper::load_public_ec_key_from_string(public_key, public_key_password); + check_public_key(pkey.get()); + } else { + throw error::ecdsa_exception(error::ecdsa_error::no_key_provided); } + if (!pkey) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); - if (!private_key.empty()) { - std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); - if (!privkey_bio) throw ecdsa_exception(error::ecdsa_error::create_mem_bio_failed); - const int len = static_cast(private_key.size()); - if (BIO_write(privkey_bio.get(), private_key.data(), len) != len) - throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); - pkey.reset(PEM_read_bio_ECPrivateKey(privkey_bio.get(), nullptr, nullptr, - const_cast(private_key_password.c_str())), - EC_KEY_free); - if (!pkey) throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); - size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); - if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) - throw ecdsa_exception(error::ecdsa_error::invalid_key_size); - } - if (!pkey) throw ecdsa_exception(error::ecdsa_error::no_key_provided); - - if (EC_KEY_check_key(pkey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); + size_t keysize = EVP_PKEY_bits(pkey.get()); + if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) + throw error::ecdsa_exception(error::ecdsa_error::invalid_key_size); } + /** * Sign jwt data * \param data The data to sign @@ -924,18 +1105,124 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - const std::string hash = generate_hash(data, ec); - if (ec) return {}; + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (!EVP_DigestSignInit(ctx.get(), nullptr, md(), nullptr, pkey.get())) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + if (!EVP_DigestUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_generation_error::digestupdate_failed; + return {}; + } + + size_t len = 0; + if (!EVP_DigestSignFinal(ctx.get(), nullptr, &len)) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + std::string res(len, '\0'); + if (!EVP_DigestSignFinal(ctx.get(), (unsigned char*)res.data(), &len)) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + + res.resize(len); + return der_to_p1363_signature(res, ec); + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on error + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + std::string der_signature = p1363_to_der_signature(signature, ec); + if (ec) { return; } + + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + if (!EVP_DigestVerifyInit(ctx.get(), nullptr, md(), nullptr, pkey.get())) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } + if (!EVP_DigestUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + +#if OPENSSL_VERSION_NUMBER < 0x10002000L + unsigned char* der_sig_data = reinterpret_cast(const_cast(der_signature.data())); +#else + const unsigned char* der_sig_data = reinterpret_cast(der_signature.data()); +#endif + auto res = + EVP_DigestVerifyFinal(ctx.get(), der_sig_data, static_cast(der_signature.length())); + if (res == 0) { + ec = error::signature_verification_error::invalid_signature; + return; + } + if (res == -1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + static void check_public_key(EVP_PKEY* pkey) { +#ifdef JWT_OPENSSL_3_0 + std::unique_ptr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); + if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_public_check(ctx.get()) != 1) { + throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + } +#else + std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); + if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); +#endif + } + + static void check_private_key(EVP_PKEY* pkey) { +#ifdef JWT_OPENSSL_3_0 + std::unique_ptr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); + if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_private_check(ctx.get()) != 1) { + throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + } +#else + std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); + if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); +#endif + } + std::string der_to_p1363_signature(const std::string& der_signature, std::error_code& ec) const { + const unsigned char* possl_signature = reinterpret_cast(der_signature.data()); std::unique_ptr sig( - ECDSA_do_sign(reinterpret_cast(hash.data()), static_cast(hash.size()), - pkey.get()), + d2i_ECDSA_SIG(nullptr, &possl_signature, static_cast(der_signature.length())), ECDSA_SIG_free); if (!sig) { - ec = error::signature_generation_error::ecdsa_do_sign_failed; + ec = error::signature_generation_error::signature_decoding_failed; return {}; } -#ifdef OPENSSL10 + +#ifdef JWT_OPENSSL_1_0_0 auto rr = helper::bn2raw(sig->r); auto rs = helper::bn2raw(sig->s); @@ -953,91 +1240,45 @@ namespace jwt { return rr + rs; } - /** - * Check if signature is valid - * \param data The data to check signature against - * \param signature Signature provided by the jwt - * \param ec Filled with details on error - */ - void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + std::string p1363_to_der_signature(const std::string& signature, std::error_code& ec) const { ec.clear(); - const std::string hash = generate_hash(data, ec); - if (ec) return; auto r = helper::raw2bn(signature.substr(0, signature.size() / 2)); auto s = helper::raw2bn(signature.substr(signature.size() / 2)); -#ifdef OPENSSL10 + ECDSA_SIG* psig; +#ifdef JWT_OPENSSL_1_0_0 ECDSA_SIG sig; sig.r = r.get(); sig.s = s.get(); - - if (ECDSA_do_verify((const unsigned char*)hash.data(), static_cast(hash.size()), &sig, - pkey.get()) != 1) { - ec = error::signature_verification_error::invalid_signature; - return; - } + psig = &sig; #else std::unique_ptr sig(ECDSA_SIG_new(), ECDSA_SIG_free); if (!sig) { ec = error::signature_verification_error::create_context_failed; - return; + return {}; } - ECDSA_SIG_set0(sig.get(), r.release(), s.release()); - - if (ECDSA_do_verify(reinterpret_cast(hash.data()), static_cast(hash.size()), - sig.get(), pkey.get()) != 1) { - ec = error::signature_verification_error::invalid_signature; - return; - } + psig = sig.get(); #endif - } - /** - * Returns the algorithm name provided to the constructor - * \return algorithm's name - */ - std::string name() const { return alg_name; } - private: - /** - * Hash the provided data using the hash function specified in constructor - * \param data Data to hash - * \return Hash of data - */ - std::string generate_hash(const std::string& data, std::error_code& ec) const { -#ifdef OPENSSL10 - std::unique_ptr ctx(EVP_MD_CTX_create(), - &EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); -#endif - if (!ctx) { - ec = error::signature_generation_error::create_context_failed; - return {}; - } - if (EVP_DigestInit(ctx.get(), md()) == 0) { - ec = error::signature_generation_error::digestinit_failed; - return {}; - } - if (EVP_DigestUpdate(ctx.get(), data.data(), data.size()) == 0) { - ec = error::signature_generation_error::digestupdate_failed; + int length = i2d_ECDSA_SIG(psig, nullptr); + if (length < 0) { + ec = error::signature_verification_error::signature_encoding_failed; return {}; } - unsigned int len = 0; - std::string res(EVP_MD_CTX_size(ctx.get()), '\0'); - if (EVP_DigestFinal( - ctx.get(), - (unsigned char*)res.data(), // NOLINT(google-readability-casting) requires `const_cast` - &len) == 0) { - ec = error::signature_generation_error::digestfinal_failed; + std::string der_signature(length, '\0'); + unsigned char* psbuffer = (unsigned char*)der_signature.data(); + length = i2d_ECDSA_SIG(psig, &psbuffer); + if (length < 0) { + ec = error::signature_verification_error::signature_encoding_failed; return {}; } - res.resize(len); - return res; + der_signature.resize(length); + return der_signature; } /// OpenSSL struct containing keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// Hash generator function const EVP_MD* (*md)(); /// algorithm's name @@ -1046,7 +1287,7 @@ namespace jwt { const size_t signature_length; }; -#ifndef OPENSSL110 +#if !defined(JWT_OPENSSL_1_0_0) && !defined(JWT_OPENSSL_1_1_0) /** * \brief Base class for EdDSA family of algorithms * @@ -1074,7 +1315,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); + throw error::ecdsa_exception(error::ecdsa_error::load_key_bio_read); } /** * Sign jwt data @@ -1084,7 +1325,7 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1100,7 +1341,7 @@ namespace jwt { // LibreSSL is the special kid in the block, as it does not support EVP_DigestSign. // OpenSSL on the otherhand does not support using EVP_DigestSignUpdate for eddsa, which is why we end up with this // mess. -#ifdef LIBRESSL_VERSION_NUMBER +#if defined(LIBRESSL_VERSION_NUMBER) || defined(LIBWOLFSSL_VERSION_HEX) ERR_clear_error(); if (EVP_DigestSignUpdate(ctx.get(), reinterpret_cast(data.data()), data.size()) != 1) { @@ -1132,7 +1373,7 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1144,7 +1385,7 @@ namespace jwt { // LibreSSL is the special kid in the block, as it does not support EVP_DigestVerify. // OpenSSL on the otherhand does not support using EVP_DigestVerifyUpdate for eddsa, which is why we end up with this // mess. -#ifdef LIBRESSL_VERSION_NUMBER +#if defined(LIBRESSL_VERSION_NUMBER) || defined(LIBWOLFSSL_VERSION_HEX) if (EVP_DigestVerifyUpdate(ctx.get(), reinterpret_cast(data.data()), data.size()) != 1) { ec = error::signature_verification_error::verifyupdate_failed; @@ -1173,7 +1414,7 @@ namespace jwt { private: /// OpenSSL struct containing keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// algorithm's name const std::string alg_name; }; @@ -1199,7 +1440,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw rsa_exception(error::rsa_error::no_key_provided); + throw error::rsa_exception(error::rsa_error::no_key_provided); } /** @@ -1210,31 +1451,43 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - auto hash = this->generate_hash(data, ec); - if (ec) return {}; - - std::unique_ptr key(EVP_PKEY_get1_RSA(pkey.get()), RSA_free); - if (!key) { - ec = error::signature_generation_error::get_key_failed; + auto md_ctx = helper::make_evp_md_ctx(); + if (!md_ctx) { + ec = error::signature_generation_error::create_context_failed; return {}; } - const int size = RSA_size(key.get()); - - std::string padded(size, 0x00); - if (RSA_padding_add_PKCS1_PSS_mgf1( - key.get(), (unsigned char*)padded.data(), reinterpret_cast(hash.data()), - md(), md(), -1) == 0) { // NOLINT(google-readability-casting) requires `const_cast` + EVP_PKEY_CTX* ctx = nullptr; + if (EVP_DigestSignInit(md_ctx.get(), &ctx, md(), nullptr, pkey.get()) != 1) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PSS_PADDING) <= 0) { ec = error::signature_generation_error::rsa_padding_failed; return {}; } +// wolfSSL does not require EVP_PKEY_CTX_set_rsa_pss_saltlen. The default behavior +// sets the salt length to the hash length. Unlike OpenSSL which exposes this functionality. +#ifndef LIBWOLFSSL_VERSION_HEX + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, -1) <= 0) { + ec = error::signature_generation_error::set_rsa_pss_saltlen_failed; + return {}; + } +#endif + if (EVP_DigestUpdate(md_ctx.get(), data.data(), data.size()) != 1) { + ec = error::signature_generation_error::digestupdate_failed; + return {}; + } + size_t size = EVP_PKEY_size(pkey.get()); std::string res(size, 0x00); - if (RSA_private_encrypt(size, reinterpret_cast(padded.data()), - (unsigned char*)res.data(), key.get(), RSA_NO_PADDING) < - 0) { // NOLINT(google-readability-casting) requires `const_cast` - ec = error::signature_generation_error::rsa_private_encrypt_failed; + if (EVP_DigestSignFinal( + md_ctx.get(), + (unsigned char*)res.data(), // NOLINT(google-readability-casting) requires `const_cast` + &size) <= 0) { + ec = error::signature_generation_error::signfinal_failed; return {}; } + return res; } @@ -1246,28 +1499,36 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); - auto hash = this->generate_hash(data, ec); - if (ec) return; - std::unique_ptr key(EVP_PKEY_get1_RSA(pkey.get()), RSA_free); - if (!key) { - ec = error::signature_verification_error::get_key_failed; + auto md_ctx = helper::make_evp_md_ctx(); + if (!md_ctx) { + ec = error::signature_verification_error::create_context_failed; return; } - const int size = RSA_size(key.get()); - - std::string sig(size, 0x00); - if (RSA_public_decrypt( - static_cast(signature.size()), reinterpret_cast(signature.data()), - (unsigned char*)sig.data(), // NOLINT(google-readability-casting) requires `const_cast` - key.get(), RSA_NO_PADDING) == 0) { - ec = error::signature_verification_error::invalid_signature; + EVP_PKEY_CTX* ctx = nullptr; + if (EVP_DigestVerifyInit(md_ctx.get(), &ctx, md(), nullptr, pkey.get()) != 1) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PSS_PADDING) <= 0) { + ec = error::signature_generation_error::rsa_padding_failed; + return; + } +// wolfSSL does not require EVP_PKEY_CTX_set_rsa_pss_saltlen. The default behavior +// sets the salt length to the hash length. Unlike OpenSSL which exposes this functionality. +#ifndef LIBWOLFSSL_VERSION_HEX + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, -1) <= 0) { + ec = error::signature_verification_error::set_rsa_pss_saltlen_failed; + return; + } +#endif + if (EVP_DigestUpdate(md_ctx.get(), data.data(), data.size()) != 1) { + ec = error::signature_verification_error::verifyupdate_failed; return; } - if (RSA_verify_PKCS1_PSS_mgf1(key.get(), reinterpret_cast(hash.data()), md(), - md(), reinterpret_cast(sig.data()), -1) == 0) { - ec = error::signature_verification_error::invalid_signature; + if (EVP_DigestVerifyFinal(md_ctx.get(), (unsigned char*)signature.data(), signature.size()) <= 0) { + ec = error::signature_verification_error::verifyfinal_failed; return; } } @@ -1278,43 +1539,8 @@ namespace jwt { std::string name() const { return alg_name; } private: - /** - * Hash the provided data using the hash function specified in constructor - * \param data Data to hash - * \return Hash of data - */ - std::string generate_hash(const std::string& data, std::error_code& ec) const { -#ifdef OPENSSL10 - std::unique_ptr ctx(EVP_MD_CTX_create(), - &EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); -#endif - if (!ctx) { - ec = error::signature_generation_error::create_context_failed; - return {}; - } - if (EVP_DigestInit(ctx.get(), md()) == 0) { - ec = error::signature_generation_error::digestinit_failed; - return {}; - } - if (EVP_DigestUpdate(ctx.get(), data.data(), data.size()) == 0) { - ec = error::signature_generation_error::digestupdate_failed; - return {}; - } - unsigned int len = 0; - std::string res(EVP_MD_CTX_size(ctx.get()), '\0'); - if (EVP_DigestFinal(ctx.get(), (unsigned char*)res.data(), &len) == - 0) { // NOLINT(google-readability-casting) requires `const_cast` - ec = error::signature_generation_error::digestfinal_failed; - return {}; - } - res.resize(len); - return res; - } - /// OpenSSL structure containing keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// Hash generator function const EVP_MD* (*md)(); /// algorithm's name @@ -1447,8 +1673,24 @@ namespace jwt { const std::string& public_key_password = "", const std::string& private_key_password = "") : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "ES512", 132) {} }; + /** + * ES256K algorithm + */ + struct es256k : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit es256k(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "ES256K", 64) {} + }; -#ifndef OPENSSL110 +#if !defined(JWT_OPENSSL_1_0_0) && !defined(JWT_OPENSSL_1_1_0) /** * Ed25519 algorithm * @@ -1571,9 +1813,6 @@ namespace jwt { #ifdef __cpp_lib_experimental_detect template class _Op, typename... _Args> using is_detected = std::experimental::is_detected<_Op, _Args...>; - - template class _Op, typename... _Args> - using is_detected_t = std::experimental::detected_t<_Op, _Args...>; #else struct nonesuch { nonesuch() = delete; @@ -1599,139 +1838,58 @@ namespace jwt { template class Op, class... Args> using is_detected = typename detector::value; - - template class Op, class... Args> - using is_detected_t = typename detector::type; #endif - template - using get_type_function = decltype(traits_type::get_type); + template + using is_signature = typename std::is_same; - template - using is_get_type_signature = - typename std::is_same, json::type(const value_type&)>; + template class Op, typename Signature> + struct is_function_signature_detected { + using type = Op; + static constexpr auto value = is_detected::value && std::is_function::value && + is_signature::value; + }; template struct supports_get_type { - static constexpr auto value = is_detected::value && - std::is_function>::value && - is_get_type_signature::value; - }; - - template - using as_object_function = decltype(traits_type::as_object); - - template - using is_as_object_signature = - typename std::is_same, object_type(const value_type&)>; - - template - struct supports_as_object { - static constexpr auto value = std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_object_signature::value; - }; - - template - using as_array_function = decltype(traits_type::as_array); - - template - using is_as_array_signature = - typename std::is_same, array_type(const value_type&)>; - - template - struct supports_as_array { - static constexpr auto value = std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_array_signature::value; - }; - - template - using as_string_function = decltype(traits_type::as_string); + template + using get_type_t = decltype(T::get_type); - template - using is_as_string_signature = - typename std::is_same, string_type(const value_type&)>; - - template - struct supports_as_string { - static constexpr auto value = std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_string_signature::value; - }; - - template - using as_number_function = decltype(traits_type::as_number); - - template - using is_as_number_signature = - typename std::is_same, number_type(const value_type&)>; - - template - struct supports_as_number { - static constexpr auto value = std::is_floating_point::value && - std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_number_signature::value; - }; - - template - using as_integer_function = decltype(traits_type::as_int); - - template - using is_as_integer_signature = - typename std::is_same, integer_type(const value_type&)>; + static constexpr auto value = + is_function_signature_detected::value; - template - struct supports_as_integer { - static constexpr auto value = std::is_signed::value && - !std::is_floating_point::value && - std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_integer_signature::value; + // Internal assertions for better feedback + static_assert(value, "traits implementation must provide `jwt::json::type get_type(const value_type&)`"); }; - template - using as_boolean_function = decltype(traits_type::as_bool); +#define JWT_CPP_JSON_TYPE_TYPE(TYPE) json_##TYPE_type +#define JWT_CPP_AS_TYPE_T(TYPE) as_##TYPE_t +#define JWT_CPP_SUPPORTS_AS(TYPE) \ + template \ + struct supports_as_##TYPE { \ + template \ + using JWT_CPP_AS_TYPE_T(TYPE) = decltype(T::as_##TYPE); \ + \ + static constexpr auto value = \ + is_function_signature_detected::value; \ + \ + static_assert(value, "traits implementation must provide `" #TYPE "_type as_" #TYPE "(const value_type&)`"); \ + } - template - using is_as_boolean_signature = - typename std::is_same, boolean_type(const value_type&)>; + JWT_CPP_SUPPORTS_AS(object); + JWT_CPP_SUPPORTS_AS(array); + JWT_CPP_SUPPORTS_AS(string); + JWT_CPP_SUPPORTS_AS(number); + JWT_CPP_SUPPORTS_AS(integer); + JWT_CPP_SUPPORTS_AS(boolean); - template - struct supports_as_boolean { - static constexpr auto value = std::is_convertible::value && - std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_boolean_signature::value; - }; +#undef JWT_CPP_JSON_TYPE_TYPE +#undef JWT_CPP_AS_TYPE_T +#undef JWT_CPP_SUPPORTS_AS template struct is_valid_traits { - // Internal assertions for better feedback - static_assert(supports_get_type::value, - "traits must provide `jwt::json::type get_type(const value_type&)`"); - static_assert(supports_as_object::value, - "traits must provide `object_type as_object(const value_type&)`"); - static_assert(supports_as_array::value, - "traits must provide `array_type as_array(const value_type&)`"); - static_assert(supports_as_string::value, - "traits must provide `string_type as_string(const value_type&)`"); - static_assert(supports_as_number::value, - "traits must provide `number_type as_number(const value_type&)`"); - static_assert( - supports_as_integer::value, - "traits must provide `integer_type as_int(const value_type&)`"); - static_assert( - supports_as_boolean::value, - "traits must provide `boolean_type as_bool(const value_type&)`"); - static constexpr auto value = supports_get_type::value && supports_as_object::value && @@ -1749,62 +1907,45 @@ namespace jwt { std::is_constructible::value && // a more generic is_copy_constructible std::is_move_constructible::value && std::is_assignable::value && std::is_copy_assignable::value && std::is_move_assignable::value; - // TODO(cmcarthur): Stream operators + // TODO(prince-chrismc): Stream operators }; - template - using has_mapped_type = typename traits_type::mapped_type; - - template - using has_key_type = typename traits_type::key_type; - - template - using has_value_type = typename traits_type::value_type; - - template - using has_iterator = typename object_type::iterator; - - template - using has_const_iterator = typename object_type::const_iterator; - - template - using is_begin_signature = - typename std::is_same().begin()), has_iterator>; + // https://stackoverflow.com/a/53967057/8480874 + template + struct is_iterable : std::false_type {}; - template - using is_begin_const_signature = - typename std::is_same().begin()), has_const_iterator>; - - template - struct supports_begin { - static constexpr auto value = - is_detected::value && is_detected::value && - is_begin_signature::value && is_begin_const_signature::value; + template + struct is_iterable())), decltype(std::end(std::declval())), +#if __cplusplus > 201402L + decltype(std::cbegin(std::declval())), decltype(std::cend(std::declval())) +#else + decltype(std::begin(std::declval())), + decltype(std::end(std::declval())) +#endif + >> : std::true_type { }; - template - using is_end_signature = - typename std::is_same().end()), has_iterator>; +#if __cplusplus > 201703L + template + inline constexpr bool is_iterable_v = is_iterable::value; +#endif - template - using is_end_const_signature = - typename std::is_same().end()), has_const_iterator>; + template + using is_count_signature = typename std::is_integral().count( + std::declval()))>; - template - struct supports_end { - static constexpr auto value = - is_detected::value && is_detected::value && - is_end_signature::value && is_end_const_signature::value; - }; + template + struct is_subcription_operator_signature : std::false_type {}; template - using is_count_signature = typename std::is_integral().count(std::declval()))>; - - template - using is_subcription_operator_signature = - typename std::is_same()[std::declval()]), - value_type&>; + struct is_subcription_operator_signature< + object_type, string_type, + void_t().operator[](std::declval()))>> : std::true_type { + // TODO(prince-chrismc): I am not convienced this is meaningful anymore + static_assert( + value, + "object_type must implementate the subscription operator '[]' taking string_type as an arguement"); + }; template using is_at_const_signature = @@ -1813,47 +1954,112 @@ namespace jwt { template struct is_valid_json_object { + template + using mapped_type_t = typename T::mapped_type; + template + using key_type_t = typename T::key_type; + template + using iterator_t = typename T::iterator; + template + using const_iterator_t = typename T::const_iterator; + static constexpr auto value = - is_detected::value && + std::is_constructible::value && + is_detected::value && std::is_same::value && - is_detected::value && - std::is_same::value && - supports_begin::value && supports_end::value && - is_count_signature::value && - is_subcription_operator_signature::value && + is_detected::value && + (std::is_same::value || + std::is_constructible::value) && + is_detected::value && is_detected::value && + is_iterable::value && is_count_signature::value && + is_subcription_operator_signature::value && is_at_const_signature::value; - - static constexpr auto supports_claims_transform = - value && is_detected::value && - std::is_same>::value; }; template struct is_valid_json_array { - static constexpr auto value = std::is_same::value; + template + using value_type_t = typename T::value_type; + + static constexpr auto value = std::is_constructible::value && + is_iterable::value && + is_detected::value && + std::is_same::value; + }; + + template + using is_substr_start_end_index_signature = + typename std::is_same().substr(std::declval(), + std::declval())), + string_type>; + + template + using is_substr_start_index_signature = + typename std::is_same().substr(std::declval())), + string_type>; + + template + using is_std_operate_plus_signature = + typename std::is_same(), std::declval())), + string_type>; + + template + struct is_valid_json_string { + static constexpr auto substr = is_substr_start_end_index_signature::value && + is_substr_start_index_signature::value; + static_assert(substr, "string_type must have a substr method taking only a start index and an overload " + "taking a start and end index, both must return a string_type"); + + static constexpr auto operator_plus = is_std_operate_plus_signature::value; + static_assert(operator_plus, + "string_type must have a '+' operator implemented which returns the concatenated string"); + + static constexpr auto value = + std::is_constructible::value && substr && operator_plus; + }; + + template + struct is_valid_json_number { + static constexpr auto value = + std::is_floating_point::value && std::is_constructible::value; + }; + + template + struct is_valid_json_integer { + static constexpr auto value = std::is_signed::value && + !std::is_floating_point::value && + std::is_constructible::value; + }; + template + struct is_valid_json_boolean { + static constexpr auto value = std::is_convertible::value && + std::is_constructible::value; }; - template + template struct is_valid_json_types { // Internal assertions for better feedback static_assert(is_valid_json_value::value, - "value type must meet basic requirements, default constructor, copyable, moveable"); + "value_type must meet basic requirements, default constructor, copyable, moveable"); static_assert(is_valid_json_object::value, "object_type must be a string_type to value_type container"); static_assert(is_valid_json_array::value, "array_type must be a container of value_type"); - static constexpr auto value = is_valid_json_object::value && - is_valid_json_value::value && - is_valid_json_array::value; + static constexpr auto value = is_valid_json_value::value && + is_valid_json_object::value && + is_valid_json_array::value && + is_valid_json_string::value && + is_valid_json_number::value && + is_valid_json_integer::value && + is_valid_json_boolean::value; }; } // namespace details /** * \brief a class to store a generic JSON value as claim * - * The default template parameters use [picojson](https://github.com/kazuho/picojson) - * * \tparam json_traits : JSON implementation traits * * \see [RFC 7519: JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) @@ -1866,12 +2072,16 @@ namespace jwt { * https://github.com/nlohmann/json/issues/774. It maybe be expanded to * support custom string types. */ - static_assert(std::is_same::value, - "string_type must be a std::string."); + static_assert(std::is_same::value || + std::is_convertible::value || + std::is_constructible::value, + "string_type must be a std::string, convertible to a std::string, or construct a std::string."); static_assert( - details::is_valid_json_types::value, + details::is_valid_json_types::value, "must staisfy json container requirements"); static_assert(details::is_valid_traits::value, "traits must satisfy requirements"); @@ -1929,11 +2139,18 @@ namespace jwt { typename json_traits::string_type as_string() const { return json_traits::as_string(val); } /** - * Get the contained JSON value as a date + * \brief Get the contained JSON value as a date + * + * If the value is a decimal, it is rounded up to the closest integer + * * \return content as date * \throw std::bad_cast Content was not a date */ - date as_date() const { return std::chrono::system_clock::from_time_t(as_int()); } + date as_date() const { + using std::chrono::system_clock; + if (get_type() == json::type::number) return system_clock::from_time_t(std::round(as_number())); + return system_clock::from_time_t(as_integer()); + } /** * Get the contained JSON value as an array @@ -1960,14 +2177,14 @@ namespace jwt { * \return content as int * \throw std::bad_cast Content was not an int */ - typename json_traits::integer_type as_int() const { return json_traits::as_int(val); } + typename json_traits::integer_type as_integer() const { return json_traits::as_integer(val); } /** * Get the contained JSON value as a bool * \return content as bool * \throw std::bad_cast Content was not a bool */ - typename json_traits::boolean_type as_bool() const { return json_traits::as_bool(val); } + typename json_traits::boolean_type as_boolean() const { return json_traits::as_boolean(val); } /** * Get the contained JSON value as a number @@ -1978,9 +2195,15 @@ namespace jwt { }; namespace error { + /** + * Attempt to parse JSON was unsuccessful + */ struct invalid_json_exception : public std::runtime_error { invalid_json_exception() : runtime_error("invalid json") {} }; + /** + * Attempt to access claim was unsuccessful + */ struct claim_not_present_exception : public std::out_of_range { claim_not_present_exception() : out_of_range("claim not found") {} }; @@ -1988,10 +2211,8 @@ namespace jwt { namespace details { template - class map_of_claims { + struct map_of_claims { typename json_traits::object_type claims; - - public: using basic_claim_t = basic_claim; using iterator = typename json_traits::object_type::iterator; using const_iterator = typename json_traits::object_type::const_iterator; @@ -2045,21 +2266,6 @@ namespace jwt { if (!has_claim(name)) throw error::claim_not_present_exception(); return basic_claim_t{claims.at(name)}; } - - std::unordered_map get_claims() const { - static_assert( - details::is_valid_json_object::supports_claims_transform, - "currently there is a limitation on the internal implemantation of the `object_type` to have an " - "`std::pair` like `value_type`"); - - std::unordered_map res; - std::transform(claims.begin(), claims.end(), std::inserter(res, res.end()), - [](const typename json_traits::object_type::value_type& val) { - return std::make_pair(val.first, basic_claim_t{val.second}); - }); - return res; - } }; } // namespace details @@ -2264,7 +2470,7 @@ namespace jwt { class decoded_jwt : public header, public payload { protected: /// Unmodifed token, as passed to constructor - const typename json_traits::string_type token; + typename json_traits::string_type token; /// Header part decoded from base64 typename json_traits::string_type header; /// Unmodified header part in base64 @@ -2291,8 +2497,8 @@ namespace jwt { * \throw std::runtime_error Base64 decoding failed or invalid json */ JWT_CLAIM_EXPLICIT decoded_jwt(const typename json_traits::string_type& token) - : decoded_jwt(token, [](const typename json_traits::string_type& token) { - return base::decode(base::pad(token)); + : decoded_jwt(token, [](const typename json_traits::string_type& str) { + return base::decode(base::pad(str)); }) {} #endif /** @@ -2360,18 +2566,34 @@ namespace jwt { */ const typename json_traits::string_type& get_signature_base64() const noexcept { return signature_base64; } /** - * Get all payload claims + * Get all payload as JSON object * \return map of claims */ - std::unordered_map get_payload_claims() const { - return this->payload_claims.get_claims(); - } + typename json_traits::object_type get_payload_json() const { return this->payload_claims.claims; } /** - * Get all header claims + * Get all header as JSON object * \return map of claims */ - std::unordered_map get_header_claims() const { - return this->header_claims.get_claims(); + typename json_traits::object_type get_header_json() const { return this->header_claims.claims; } + /** + * Get a payload claim by name + * + * \param name the name of the desired claim + * \return Requested claim + * \throw jwt::error::claim_not_present_exception if the claim was not present + */ + basic_claim_t get_payload_claim(const typename json_traits::string_type& name) const { + return this->payload_claims.get_claim(name); + } + /** + * Get a header claim by name + * + * \param name the name of the desired claim + * \return Requested claim + * \throw jwt::error::claim_not_present_exception if the claim was not present + */ + basic_claim_t get_header_claim(const typename json_traits::string_type& name) const { + return this->header_claims.get_claim(name); } }; @@ -2428,8 +2650,9 @@ namespace jwt { return *this; } /** - * Set algorithm claim + * \brief Set algorithm claim * You normally don't need to do this, as the algorithm is automatically set if you don't change it. + * * \param str Name of algorithm * \return *this to allow for method chaining */ @@ -2453,7 +2676,8 @@ namespace jwt { return set_header_claim("cty", typename json_traits::value_type(str)); } /** - * Set key id claim + * \brief Set key id claim + * * \param str Key id to set * \return *this to allow for method chaining */ @@ -2604,12 +2828,204 @@ namespace jwt { #endif }; + namespace verify_ops { + /** + * This is the base container which holds the token that need to be verified + */ + template + struct verify_context { + verify_context(date ctime, const decoded_jwt& j, size_t l) + : current_time(ctime), jwt(j), default_leeway(l) {} + // Current time, retrieved from the verifiers clock and cached for performance and consistency + date current_time; + // The jwt passed to the verifier + const decoded_jwt& jwt; + // The configured default leeway for this verification + size_t default_leeway{0}; + + // The claim key to apply this comparision on + typename json_traits::string_type claim_key{}; + + // Helper method to get a claim from the jwt in this context + basic_claim get_claim(bool in_header, std::error_code& ec) const { + if (in_header) { + if (!jwt.has_header_claim(claim_key)) { + ec = error::token_verification_error::missing_claim; + return {}; + } + return jwt.get_header_claim(claim_key); + } else { + if (!jwt.has_payload_claim(claim_key)) { + ec = error::token_verification_error::missing_claim; + return {}; + } + return jwt.get_payload_claim(claim_key); + } + } + basic_claim get_claim(bool in_header, json::type t, std::error_code& ec) const { + auto c = get_claim(in_header, ec); + if (ec) return {}; + if (c.get_type() != t) { + ec = error::token_verification_error::claim_type_missmatch; + return {}; + } + return c; + } + basic_claim get_claim(std::error_code& ec) const { return get_claim(false, ec); } + basic_claim get_claim(json::type t, std::error_code& ec) const { + return get_claim(false, t, ec); + } + }; + + /** + * This is the default operation and does case sensitive matching + */ + template + struct equals_claim { + const basic_claim expected; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto jc = ctx.get_claim(in_header, expected.get_type(), ec); + if (ec) return; + const bool matches = [&]() { + switch (expected.get_type()) { + case json::type::boolean: return expected.as_boolean() == jc.as_boolean(); + case json::type::integer: return expected.as_integer() == jc.as_integer(); + case json::type::number: return expected.as_number() == jc.as_number(); + case json::type::string: return expected.as_string() == jc.as_string(); + case json::type::array: + case json::type::object: + return json_traits::serialize(expected.to_json()) == json_traits::serialize(jc.to_json()); + default: throw std::logic_error("internal error, should be unreachable"); + } + }(); + if (!matches) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + } + }; + + /** + * Checks that the current time is before the time specified in the given + * claim. This is identical to how the "exp" check works. + */ + template + struct date_before_claim { + const size_t leeway; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto jc = ctx.get_claim(in_header, json::type::integer, ec); + if (ec) return; + auto c = jc.as_date(); + if (ctx.current_time > c + std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + } + } + }; + + /** + * Checks that the current time is after the time specified in the given + * claim. This is identical to how the "nbf" and "iat" check works. + */ + template + struct date_after_claim { + const size_t leeway; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto jc = ctx.get_claim(in_header, json::type::integer, ec); + if (ec) return; + auto c = jc.as_date(); + if (ctx.current_time < c - std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + } + } + }; + + /** + * Checks if the given set is a subset of the set inside the token. + * If the token value is a string it is traited as a set of a single element. + * The comparison is case sensitive. + */ + template + struct is_subset_claim { + const typename basic_claim::set_t expected; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto c = ctx.get_claim(in_header, ec); + if (ec) return; + if (c.get_type() == json::type::string) { + if (expected.size() != 1 || *expected.begin() != c.as_string()) { + ec = error::token_verification_error::audience_missmatch; + return; + } + } else if (c.get_type() == json::type::array) { + auto jc = c.as_set(); + for (auto& e : expected) { + if (jc.find(e) == jc.end()) { + ec = error::token_verification_error::audience_missmatch; + return; + } + } + } else { + ec = error::token_verification_error::claim_type_missmatch; + return; + } + } + }; + + /** + * Checks if the claim is a string and does an case insensitive comparison. + */ + template + struct insensitive_string_claim { + const typename json_traits::string_type expected; + std::locale locale; + insensitive_string_claim(const typename json_traits::string_type& e, std::locale loc) + : expected(to_lower_unicode(e, loc)), locale(loc) {} + + void operator()(const verify_context& ctx, std::error_code& ec) const { + const auto c = ctx.get_claim(in_header, json::type::string, ec); + if (ec) return; + if (to_lower_unicode(c.as_string(), locale) != expected) { + ec = error::token_verification_error::claim_value_missmatch; + } + } + + static std::string to_lower_unicode(const std::string& str, const std::locale& loc) { +#if __cplusplus > 201103L + std::wstring_convert, wchar_t> conv; + auto wide = conv.from_bytes(str); + auto& f = std::use_facet>(loc); + f.tolower(&wide[0], &wide[0] + wide.size()); + return conv.to_bytes(wide); +#else + std::string result; + std::transform(str.begin(), str.end(), std::back_inserter(result), + [&loc](unsigned char c) { return std::tolower(c, loc); }); + return result; +#endif + } + }; + } // namespace verify_ops + /** * Verifier class used to check if a decoded token contains all claims required by your application and has a valid * signature. */ template class verifier { + public: + using basic_claim_t = basic_claim; + /** + * Verification function + * + * This gets passed the current verifier, a reference to the decoded jwt, a reference to the key of this claim, + * as well as a reference to an error_code. + * The function checks if the actual value matches certain rules (e.g. equality to value x) and sets the error_code if + * it does not. Once a non zero error_code is encountered the verification stops and this error_code becomes the result + * returned from verify + */ + using verify_check_fn_t = + std::function&, std::error_code& ec)>; + + private: struct algo_base { virtual ~algo_base() = default; virtual void verify(const std::string& data, const std::string& sig, std::error_code& ec) = 0; @@ -2622,10 +3038,8 @@ namespace jwt { alg.verify(data, sig, ec); } }; - - using basic_claim_t = basic_claim; /// Required claims - std::unordered_map claims; + std::unordered_map claims; /// Leeway time for exp, nbf and iat size_t default_leeway = 0; /// Instance of clock type @@ -2638,7 +3052,29 @@ namespace jwt { * Constructor for building a new verifier instance * \param c Clock instance */ - explicit verifier(Clock c) : clock(c) {} + explicit verifier(Clock c) : clock(c) { + claims["exp"] = [](const verify_ops::verify_context& ctx, std::error_code& ec) { + if (!ctx.jwt.has_expires_at()) return; + auto exp = ctx.jwt.get_expires_at(); + if (ctx.current_time > exp + std::chrono::seconds(ctx.default_leeway)) { + ec = error::token_verification_error::token_expired; + } + }; + claims["iat"] = [](const verify_ops::verify_context& ctx, std::error_code& ec) { + if (!ctx.jwt.has_issued_at()) return; + auto iat = ctx.jwt.get_issued_at(); + if (ctx.current_time < iat - std::chrono::seconds(ctx.default_leeway)) { + ec = error::token_verification_error::token_expired; + } + }; + claims["nbf"] = [](const verify_ops::verify_context& ctx, std::error_code& ec) { + if (!ctx.jwt.has_not_before()) return; + auto nbf = ctx.jwt.get_not_before(); + if (ctx.current_time < nbf - std::chrono::seconds(ctx.default_leeway)) { + ec = error::token_verification_error::token_expired; + } + }; + } /** * Set default leeway to use. @@ -2656,7 +3092,8 @@ namespace jwt { * \return *this to allow chaining */ verifier& expires_at_leeway(size_t leeway) { - return with_claim("exp", basic_claim_t(std::chrono::system_clock::from_time_t(leeway))); + claims["exp"] = verify_ops::date_before_claim{leeway}; + return *this; } /** * Set leeway for not before. @@ -2665,7 +3102,8 @@ namespace jwt { * \return *this to allow chaining */ verifier& not_before_leeway(size_t leeway) { - return with_claim("nbf", basic_claim_t(std::chrono::system_clock::from_time_t(leeway))); + claims["nbf"] = verify_ops::date_after_claim{leeway}; + return *this; } /** * Set leeway for issued at. @@ -2674,8 +3112,25 @@ namespace jwt { * \return *this to allow chaining */ verifier& issued_at_leeway(size_t leeway) { - return with_claim("iat", basic_claim_t(std::chrono::system_clock::from_time_t(leeway))); + claims["iat"] = verify_ops::date_after_claim{leeway}; + return *this; } + + /** + * Set an type to check for. + * + * According to [RFC 7519 Section 5.1](https://datatracker.ietf.org/doc/html/rfc7519#section-5.1), + * This parameter is ignored by JWT implementations; any processing of this parameter is performed by the JWT application. + * Check is casesensitive. + * + * \param type Type Header Parameter to check for. + * \param locale Localization functionality to use when comapring + * \return *this to allow chaining + */ + verifier& with_type(const typename json_traits::string_type& type, std::locale locale = std::locale{}) { + return with_claim("typ", verify_ops::insensitive_string_claim{type, std::move(locale)}); + } + /** * Set an issuer to check for. * Check is casesensitive. @@ -2685,6 +3140,7 @@ namespace jwt { verifier& with_issuer(const typename json_traits::string_type& iss) { return with_claim("iss", basic_claim_t(iss)); } + /** * Set a subject to check for. * Check is casesensitive. @@ -2701,7 +3157,8 @@ namespace jwt { * \return *this to allow chaining */ verifier& with_audience(const typename basic_claim_t::set_t& aud) { - return with_claim("aud", basic_claim_t(aud)); + claims["aud"] = verify_ops::is_subset_claim{aud}; + return *this; } /** * Set an audience to check for. @@ -2710,7 +3167,9 @@ namespace jwt { * \return *this to allow chaining */ verifier& with_audience(const typename json_traits::string_type& aud) { - return with_claim("aud", basic_claim_t(aud)); + typename basic_claim_t::set_t s; + s.insert(aud); + return with_audience(s); } /** * Set an id to check for. @@ -2719,15 +3178,26 @@ namespace jwt { * \return *this to allow chaining */ verifier& with_id(const typename json_traits::string_type& id) { return with_claim("jti", basic_claim_t(id)); } + + /** + * Specify a claim to check for using the specified operation. + * \param name Name of the claim to check for + * \param fn Function to use for verifying the claim + * \return *this to allow chaining + */ + verifier& with_claim(const typename json_traits::string_type& name, verify_check_fn_t fn) { + claims[name] = fn; + return *this; + } + /** - * Specify a claim to check for. + * Specify a claim to check for equality (both type & value). * \param name Name of the claim to check for * \param c Claim to check for * \return *this to allow chaining */ verifier& with_claim(const typename json_traits::string_type& name, basic_claim_t c) { - claims[name] = c; - return *this; + return with_claim(name, verify_ops::equals_claim{c}); } /** @@ -2768,111 +3238,287 @@ namespace jwt { algs.at(algo)->verify(data, sig, ec); if (ec) return; - auto assert_claim_eq = [](const decoded_jwt& jwt, const typename json_traits::string_type& key, - const basic_claim_t& c, std::error_code& ec) { - if (!jwt.has_payload_claim(key)) { - ec = error::token_verification_error::missing_claim; - return; - } - auto jc = jwt.get_payload_claim(key); - if (jc.get_type() != c.get_type()) { - ec = error::token_verification_error::claim_type_missmatch; - return; - } - if (c.get_type() == json::type::integer) { - if (c.as_date() != jc.as_date()) { - ec = error::token_verification_error::claim_value_missmatch; - return; - } - } else if (c.get_type() == json::type::array) { - auto s1 = c.as_set(); - auto s2 = jc.as_set(); - if (s1.size() != s2.size()) { - ec = error::token_verification_error::claim_value_missmatch; - return; - } - auto it1 = s1.cbegin(); - auto it2 = s2.cbegin(); - while (it1 != s1.cend() && it2 != s2.cend()) { - if (*it1++ != *it2++) { - ec = error::token_verification_error::claim_value_missmatch; - return; - } - } - } else if (c.get_type() == json::type::object) { - if (json_traits::serialize(c.to_json()) != json_traits::serialize(jc.to_json())) { - ec = error::token_verification_error::claim_value_missmatch; - return; - } - } else if (c.get_type() == json::type::string) { - if (c.as_string() != jc.as_string()) { - ec = error::token_verification_error::claim_value_missmatch; - return; - } - } else - throw std::logic_error("internal error, should be unreachable"); - }; - - auto time = clock.now(); - - if (jwt.has_expires_at()) { - auto leeway = claims.count("exp") == 1 - ? std::chrono::system_clock::to_time_t(claims.at("exp").as_date()) - : default_leeway; - auto exp = jwt.get_expires_at(); - if (time > exp + std::chrono::seconds(leeway)) { - ec = error::token_verification_error::token_expired; - return; - } - } - if (jwt.has_issued_at()) { - auto leeway = claims.count("iat") == 1 - ? std::chrono::system_clock::to_time_t(claims.at("iat").as_date()) - : default_leeway; - auto iat = jwt.get_issued_at(); - if (time < iat - std::chrono::seconds(leeway)) { - ec = error::token_verification_error::token_expired; - return; - } - } - if (jwt.has_not_before()) { - auto leeway = claims.count("nbf") == 1 - ? std::chrono::system_clock::to_time_t(claims.at("nbf").as_date()) - : default_leeway; - auto nbf = jwt.get_not_before(); - if (time < nbf - std::chrono::seconds(leeway)) { - ec = error::token_verification_error::token_expired; - return; - } - } + verify_ops::verify_context ctx{clock.now(), jwt, default_leeway}; for (auto& c : claims) { - if (c.first == "exp" || c.first == "iat" || c.first == "nbf") { - // Nothing to do here, already checked - } else if (c.first == "aud") { - if (!jwt.has_audience()) { - ec = error::token_verification_error::audience_missmatch; - return; - } - auto aud = jwt.get_audience(); - typename basic_claim_t::set_t expected = {}; - if (c.second.get_type() == json::type::string) - expected = {c.second.as_string()}; - else - expected = c.second.as_set(); - for (auto& e : expected) { - if (aud.count(e) == 0) { - ec = error::token_verification_error::audience_missmatch; - return; - } - } - } else { - assert_claim_eq(jwt, c.first, c.second, ec); - if (ec) return; - } + ctx.claim_key = c.first; + c.second(ctx, ec); + if (ec) return; } } }; + /** + * \brief JSON Web Key + * + * https://tools.ietf.org/html/rfc7517 + * + * A JSON object that represents a cryptographic key. The members of + * the object represent properties of the key, including its value. + */ + template + class jwk { + using basic_claim_t = basic_claim; + const details::map_of_claims jwk_claims; + + public: + JWT_CLAIM_EXPLICIT jwk(const typename json_traits::string_type& str) + : jwk_claims(details::map_of_claims::parse_claims(str)) {} + + JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json) + : jwk_claims(json_traits::as_object(json)) {} + + /** + * Get key type claim + * + * This returns the general type (e.g. RSA or EC), not a specific algorithm value. + * \return key type as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_key_type() const { return get_jwk_claim("kty").as_string(); } + + /** + * Get public key usage claim + * \return usage parameter as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_use() const { return get_jwk_claim("use").as_string(); } + + /** + * Get key operation types claim + * \return key operation types as a set of strings + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename basic_claim_t::set_t get_key_operations() const { return get_jwk_claim("key_ops").as_set(); } + + /** + * Get algorithm claim + * \return algorithm as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_algorithm() const { return get_jwk_claim("alg").as_string(); } + + /** + * Get key id claim + * \return key id as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_key_id() const { return get_jwk_claim("kid").as_string(); } + + /** + * \brief Get curve claim + * + * https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1 + * https://www.iana.org/assignments/jose/jose.xhtml#table-web-key-elliptic-curve + * + * \return curve as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_curve() const { return get_jwk_claim("crv").as_string(); } + + /** + * Get x5c claim + * \return x5c as an array + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a array (Should not happen in a valid token) + */ + typename json_traits::array_type get_x5c() const { return get_jwk_claim("x5c").as_array(); }; + + /** + * Get X509 URL claim + * \return x5u as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5u() const { return get_jwk_claim("x5u").as_string(); }; + + /** + * Get X509 thumbprint claim + * \return x5t as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5t() const { return get_jwk_claim("x5t").as_string(); }; + + /** + * Get X509 SHA256 thumbprint claim + * \return x5t#S256 as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5t_sha256() const { return get_jwk_claim("x5t#S256").as_string(); }; + + /** + * Get x5c claim as a string + * \return x5c as an string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5c_key_value() const { + auto x5c_array = get_jwk_claim("x5c").as_array(); + if (x5c_array.size() == 0) throw error::claim_not_present_exception(); + + return json_traits::as_string(x5c_array.front()); + }; + + /** + * Check if a key type is present ("kty") + * \return true if present, false otherwise + */ + bool has_key_type() const noexcept { return has_jwk_claim("kty"); } + + /** + * Check if a public key usage indication is present ("use") + * \return true if present, false otherwise + */ + bool has_use() const noexcept { return has_jwk_claim("use"); } + + /** + * Check if a key operations parameter is present ("key_ops") + * \return true if present, false otherwise + */ + bool has_key_operations() const noexcept { return has_jwk_claim("key_ops"); } + + /** + * Check if algortihm is present ("alg") + * \return true if present, false otherwise + */ + bool has_algorithm() const noexcept { return has_jwk_claim("alg"); } + + /** + * Check if curve is present ("crv") + * \return true if present, false otherwise + */ + bool has_curve() const noexcept { return has_jwk_claim("crv"); } + + /** + * Check if key id is present ("kid") + * \return true if present, false otherwise + */ + bool has_key_id() const noexcept { return has_jwk_claim("kid"); } + + /** + * Check if X509 URL is present ("x5u") + * \return true if present, false otherwise + */ + bool has_x5u() const noexcept { return has_jwk_claim("x5u"); } + + /** + * Check if X509 Chain is present ("x5c") + * \return true if present, false otherwise + */ + bool has_x5c() const noexcept { return has_jwk_claim("x5c"); } + + /** + * Check if a X509 thumbprint is present ("x5t") + * \return true if present, false otherwise + */ + bool has_x5t() const noexcept { return has_jwk_claim("x5t"); } + + /** + * Check if a X509 SHA256 thumbprint is present ("x5t#S256") + * \return true if present, false otherwise + */ + bool has_x5t_sha256() const noexcept { return has_jwk_claim("x5t#S256"); } + + /** + * Check if a jwks claim is present + * \return true if claim was present, false otherwise + */ + bool has_jwk_claim(const typename json_traits::string_type& name) const noexcept { + return jwk_claims.has_claim(name); + } + + /** + * Get jwks claim + * \return Requested claim + * \throw std::runtime_error If claim was not present + */ + basic_claim_t get_jwk_claim(const typename json_traits::string_type& name) const { + return jwk_claims.get_claim(name); + } + + bool empty() const noexcept { return jwk_claims.empty(); } + + /** + * Get all jwk claims + * \return Map of claims + */ + typename json_traits::object_type get_claims() const { return this->jwk_claims.claims; } + }; + + /** + * \brief JWK Set + * + * https://tools.ietf.org/html/rfc7517 + * + * A JSON object that represents a set of JWKs. The JSON object MUST + * have a "keys" member, which is an array of JWKs. + * + * This container takes a JWKs and simplifies it to a vector of JWKs + */ + template + class jwks { + public: + using jwk_t = jwk; + using jwt_vector_t = std::vector; + using iterator = typename jwt_vector_t::iterator; + using const_iterator = typename jwt_vector_t::const_iterator; + + JWT_CLAIM_EXPLICIT jwks(const typename json_traits::string_type& str) { + typename json_traits::value_type parsed_val; + if (!json_traits::parse(parsed_val, str)) throw error::invalid_json_exception(); + + const details::map_of_claims jwks_json = json_traits::as_object(parsed_val); + if (!jwks_json.has_claim("keys")) throw error::invalid_json_exception(); + + auto jwk_list = jwks_json.get_claim("keys").as_array(); + std::transform(jwk_list.begin(), jwk_list.end(), std::back_inserter(jwk_claims), + [](const typename json_traits::value_type& val) { return jwk_t{val}; }); + } + + iterator begin() { return jwk_claims.begin(); } + iterator end() { return jwk_claims.end(); } + const_iterator cbegin() const { return jwk_claims.begin(); } + const_iterator cend() const { return jwk_claims.end(); } + const_iterator begin() const { return jwk_claims.begin(); } + const_iterator end() const { return jwk_claims.end(); } + + /** + * Check if a jwk with the kid is present + * \return true if jwk was present, false otherwise + */ + bool has_jwk(const typename json_traits::string_type& key_id) const noexcept { + return find_by_kid(key_id) != end(); + } + + /** + * Get jwk + * \return Requested jwk by key_id + * \throw std::runtime_error If jwk was not present + */ + jwk_t get_jwk(const typename json_traits::string_type& key_id) const { + const auto maybe = find_by_kid(key_id); + if (maybe == end()) throw error::claim_not_present_exception(); + return *maybe; + } + + private: + jwt_vector_t jwk_claims; + + const_iterator find_by_kid(const typename json_traits::string_type& key_id) const noexcept { + return std::find_if(cbegin(), cend(), [key_id](const jwk_t& jwk) { + if (!jwk.has_key_id()) { return false; } + return jwk.get_key_id() == key_id; + }); + } + }; + /** * Create a verifier using the given clock * \param c Clock instance to use @@ -2890,6 +3536,16 @@ namespace jwt { date now() const { return date::clock::now(); } }; + /** + * Create a verifier using the given clock + * \param c Clock instance to use + * \return verifier instance + */ + template + verifier verify(default_clock c = {}) { + return verifier(c); + } + /** * Return a builder instance to create a new token */ @@ -2923,108 +3579,15 @@ namespace jwt { return decoded_jwt(token); } -#ifndef JWT_DISABLE_PICOJSON - struct picojson_traits { - using value_type = picojson::value; - using object_type = picojson::object; - using array_type = picojson::array; - using string_type = std::string; - using number_type = double; - using integer_type = int64_t; - using boolean_type = bool; - - static json::type get_type(const picojson::value& val) { - using json::type; - if (val.is()) return type::boolean; - if (val.is()) return type::integer; - if (val.is()) return type::number; - if (val.is()) return type::string; - if (val.is()) return type::array; - if (val.is()) return type::object; - - throw std::logic_error("invalid type"); - } - - static picojson::object as_object(const picojson::value& val) { - if (!val.is()) throw std::bad_cast(); - return val.get(); - } - - static std::string as_string(const picojson::value& val) { - if (!val.is()) throw std::bad_cast(); - return val.get(); - } - - static picojson::array as_array(const picojson::value& val) { - if (!val.is()) throw std::bad_cast(); - return val.get(); - } - - static int64_t as_int(const picojson::value& val) { - if (!val.is()) throw std::bad_cast(); - return val.get(); - } - - static bool as_bool(const picojson::value& val) { - if (!val.is()) throw std::bad_cast(); - return val.get(); - } - - static double as_number(const picojson::value& val) { - if (!val.is()) throw std::bad_cast(); - return val.get(); - } - - static bool parse(picojson::value& val, const std::string& str) { return picojson::parse(val, str).empty(); } - - static std::string serialize(const picojson::value& val) { return val.serialize(); } - }; - - /** - * Default JSON claim - * - * This type is the default specialization of the \ref basic_claim class which - * uses the standard template types. - */ - using claim = basic_claim; - - /** - * Create a verifier using the default clock - * \return verifier instance - */ - inline verifier verify() { - return verify(default_clock{}); + template + jwk parse_jwk(const typename json_traits::string_type& token) { + return jwk(token); } - /** - * Return a picojson builder instance to create a new token - */ - inline builder create() { return builder(); } -#ifndef JWT_DISABLE_BASE64 - /** - * Decode a token - * \param token Token to decode - * \return Decoded token - * \throw std::invalid_argument Token is not in correct format - * \throw std::runtime_error Base64 decoding failed or invalid json - */ - inline decoded_jwt decode(const std::string& token) { return decoded_jwt(token); } -#endif - /** - * Decode a token - * \tparam Decode is callabled, taking a string_type and returns a string_type. - * It should ensure the padding of the input and then base64url decode and - * return the results. - * \param token Token to decode - * \param decode The token to parse - * \return Decoded token - * \throw std::invalid_argument Token is not in correct format - * \throw std::runtime_error Base64 decoding failed or invalid json - */ - template - decoded_jwt decode(const std::string& token, Decode decode) { - return decoded_jwt(token, decode); + + template + jwks parse_jwks(const typename json_traits::string_type& token) { + return jwks(token); } -#endif } // namespace jwt template @@ -3037,4 +3600,8 @@ std::ostream& operator<<(std::ostream& os, const jwt::basic_claim& return os << c.to_json(); } +#ifndef JWT_DISABLE_PICOJSON +#include "traits/kazuho-picojson/defaults.h" +#endif + #endif From eac8e695d934bc8ceaff9a2e16f345187d66bdb2 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 28 Apr 2023 14:55:43 +0100 Subject: [PATCH 016/121] Remove unnecessary copies --- Development/nmos/authorization_behaviour.cpp | 2 +- Development/nmos/jwk_utils.cpp | 2 +- Development/nmos/jwks_uri_api.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp index 115a6763b..41ebf13de 100644 --- a/Development/nmos/authorization_behaviour.cpp +++ b/Development/nmos/authorization_behaviour.cpp @@ -91,7 +91,7 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Retrieved authorization clients: " << utility::us2s(auth_clients.serialize()) << " from non-volatile memory"; - for (const auto auth_client : auth_clients.as_array()) + for (const auto& auth_client : auth_clients.as_array()) { nmos::experimental::update_client_metadata(authorization_state, auth_client.at(nmos::experimental::fields::authorization_server_uri).as_string(), nmos::experimental::fields::client_metadata(auth_client)); } diff --git a/Development/nmos/jwk_utils.cpp b/Development/nmos/jwk_utils.cpp index f56591c25..1e80ae144 100644 --- a/Development/nmos/jwk_utils.cpp +++ b/Development/nmos/jwk_utils.cpp @@ -325,7 +325,7 @@ namespace nmos { using ssl::experimental::BIO_ptr; - for (const auto private_key_ : private_keys) + for (const auto& private_key_ : private_keys) { const std::string buffer{ utility::us2s(private_key_) }; BIO_ptr bio(BIO_new_mem_buf((void*)buffer.c_str(), (int)buffer.length()), &BIO_free); diff --git a/Development/nmos/jwks_uri_api.cpp b/Development/nmos/jwks_uri_api.cpp index 2f8e408f7..2e29213a9 100644 --- a/Development/nmos/jwks_uri_api.cpp +++ b/Development/nmos/jwks_uri_api.cpp @@ -41,7 +41,7 @@ namespace nmos }); int idx = 0; - for (const auto rsa_private_key : rsa_private_keys) + for (const auto& rsa_private_key : rsa_private_keys) { const auto keyid = std::to_string(++idx); const auto jwk = details::private_key_to_jwk(rsa_private_key, utility::s2us(keyid)); From 9f80d381d6e97677b5251ba8a3fa3aeb14debea3 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 28 Apr 2023 16:08:09 +0100 Subject: [PATCH 017/121] Using std::ignore to avoid warnings from unused return value --- Development/nmos/authorization_handlers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 628d73883..e49fcb066 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -199,7 +199,7 @@ namespace nmos #else browser_cmd = "xdg-open \"" + utility::us2s(authorization_code_uri.to_string()) + "\""; #endif - system(browser_cmd.c_str()); + std::ignore = system(browser_cmd.c_str()); // TODO: process Authorization Server error response // notify authorization_code_flow in the authorization_behaviour thread From 3c1a895a5c11d8f253df7d9d1ffec94ba4cb5a5c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 2 May 2023 13:40:25 +0100 Subject: [PATCH 018/121] Add comments to show the default of the `authorization_redirect_port` and `jwks_uri_port` can be overrided by the `http_port` settings --- Development/nmos/settings.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 68a487577..b952d742d 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -411,6 +411,7 @@ namespace nmos const web::json::field_as_string_or authorization_flow{ U("authorization_flow"), U("authorization_code") }; // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration + // see http_port const web::json::field_as_integer_or authorization_redirect_port{ U("authorization_redirect_port"), 3218 }; // initial_access_token [node]: initial access token giving access to the client registration endpoint for non-opened registration @@ -425,6 +426,7 @@ namespace nmos const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic")}; // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration + // see http_port const web::json::field_as_integer_or jwks_uri_port{ U("jwks_uri_port"), 3218 }; // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) From 43aac272e4b5cad02f41ee196bed4708890a44ba Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 2 May 2023 13:57:01 +0100 Subject: [PATCH 019/121] Do tests with authorization enable --- .github/workflows/build-test.yml | 20 ++++++++++++++++---- .github/workflows/src/amwa-test.yml | 10 ++++++++-- Sandbox/run_nmos_testing.sh | 11 ++++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ed8737899..c8ece87a2 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -344,10 +344,16 @@ jobs: git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing - # Configure the Testing Tool so all APIs are tested with TLS - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py + # Configure the Testing Tool so all APIs are tested with TLS and authorization + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py + # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + hostname=nmos-api.local + else + hostname=api.testsuite.nmos.tv + printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl @@ -853,10 +859,16 @@ jobs: git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing - # Configure the Testing Tool so all APIs are tested with TLS - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py + # Configure the Testing Tool so all APIs are tested with TLS and authorization + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py + # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + hostname=nmos-api.local + else + hostname=api.testsuite.nmos.tv + printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index cca0fa18d..2e81e2a6d 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -19,10 +19,16 @@ git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing - # Configure the Testing Tool so all APIs are tested with TLS - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py + # Configure the Testing Tool so all APIs are tested with TLS and authorization + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py + # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + hostname=nmos-api.local + else + hostname=api.testsuite.nmos.tv + printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 9e45dcc6a..3ca00c999 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -110,6 +110,15 @@ fi if [[ "${config_auth}" == "True" ]]; then echo "Running Auth tests" auth=true + common_params+=",\ + \"server_authorization\":true\ + " + node_params=",\ + \"client_authorization\":true,\ + \"authorization_flow\":\"client_credentials\",\ + \"authorization_scopes\":[\"registration\"],\ + \"token_endpoint_auth_method\":\"private_key_jwt\"\ + " else echo "Running non-Auth tests" auth=false @@ -127,7 +136,7 @@ else (( expected_disabled_IS_09_01+=7 )) fi -"${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params}}" > ${results_dir}/nodeoutput 2>&1 & +"${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params} ${node_params}}" > ${results_dir}/nodeoutput 2>&1 & NODE_PID=$! function do_run_test() { From 7fe8597caf3754e6803fc57ba7dec1bf219b5af6 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 2 May 2023 14:31:46 +0100 Subject: [PATCH 020/121] Fix if...else...fi statement --- .github/workflows/build-test.yml | 2 ++ .github/workflows/src/amwa-test.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c8ece87a2..d8c411354 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -353,6 +353,7 @@ jobs: hostname=nmos-api.local else hostname=api.testsuite.nmos.tv + fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl @@ -868,6 +869,7 @@ jobs: hostname=nmos-api.local else hostname=api.testsuite.nmos.tv + fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 2e81e2a6d..ac2ea972a 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -28,6 +28,7 @@ hostname=nmos-api.local else hostname=api.testsuite.nmos.tv + fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl From 0e40511bcf7b1bcb0a4537b1b6cdd9973898a649 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 2 May 2023 16:31:11 +0100 Subject: [PATCH 021/121] IS-09-01, test_06 to test_12 are disable for authorization test --- Sandbox/run_nmos_testing.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 3ca00c999..f0068f799 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -119,6 +119,8 @@ if [[ "${config_auth}" == "True" ]]; then \"authorization_scopes\":[\"registration\"],\ \"token_endpoint_auth_method\":\"private_key_jwt\"\ " + # 7 test cases test_06 to test_12 + (( expected_disabled_IS_09_01+=7 )) else echo "Running non-Auth tests" auth=false From 807bae6a0d2db9531547e03cce5198e3f81a57eb Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 3 May 2023 16:57:21 +0100 Subject: [PATCH 022/121] Enable authorization for windows and mac --- Development/nmos-cpp-node/main.cpp | 8 -------- Development/nmos-cpp-registry/main.cpp | 4 ---- 2 files changed, 12 deletions(-) diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 5cba94c5c..a6b7998de 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -117,7 +117,6 @@ int main(int argc, char* argv[]) // only implement communication with Authorization server if IS-10 is required // cf. preprocessor conditions in nmos::make_node_api, nmos::make_connection_api, nmos::make_events_api, nmos::make_channelmapping_api, make_events_ws_validate_handler -#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) nmos::experimental::authorization_state authorization_state; if (nmos::experimental::fields::server_authorization(node_model.settings)) { @@ -125,8 +124,6 @@ int main(int argc, char* argv[]) .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(node_model, authorization_state, gate)) .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(node_model, authorization_state, gate)); } -#endif -#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) if (nmos::experimental::fields::client_authorization(node_model.settings)) { node_implementation @@ -137,7 +134,6 @@ int main(int argc, char* argv[]) .on_load_rsa_private_keys(nmos::make_load_rsa_private_keys_handler(node_model.settings, gate)) // may be omitted, only required for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint .on_request_authorization_code(nmos::experimental::make_request_authorization_code_handler(gate)); // may be omitted, only required for OAuth client which is using the Authorization Code Flow to obtain the access token } -#endif // Set up the node server @@ -159,7 +155,6 @@ int main(int argc, char* argv[]) #endif // only implement communication with Authorization server if IS-10 is required -#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) if (nmos::experimental::fields::client_authorization(node_model.settings)) { std::map api_routers; @@ -209,7 +204,6 @@ int main(int argc, char* argv[]) } } } -#endif if (!nmos::experimental::fields::http_trace(node_model.settings)) { @@ -222,7 +216,6 @@ int main(int argc, char* argv[]) } // only implement communication with Authorization server if IS-10 is required -#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) if (nmos::experimental::fields::client_authorization(node_model.settings) || nmos::experimental::fields::server_authorization(node_model.settings)) { // IS-10 client registration, fetch access token, and fetch authorization server token public key @@ -244,7 +237,6 @@ int main(int argc, char* argv[]) node_server.thread_functions.push_back([&, load_ca_certificates] { nmos::experimental::authorization_token_issuer_thread(node_model, authorization_state, load_ca_certificates, gate); }); } } -#endif // Open the API ports and start up node operation (including the DNS-SD advertisements) diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index 010391388..f2a5d50c0 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -113,7 +113,6 @@ int main(int argc, char* argv[]) // only implement communication with Authorization server if IS-10 is required // cf. preprocessor conditions in nmos::make_registration_api, nmos::make_query_api, make_query_ws_validate_handler -#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) nmos::experimental::authorization_state authorization_state; if (nmos::experimental::fields::server_authorization(registry_model.settings)) { @@ -121,7 +120,6 @@ int main(int argc, char* argv[]) .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(registry_model, authorization_state, gate)) .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(registry_model, authorization_state, gate)); } -#endif // Set up the registry server @@ -151,14 +149,12 @@ int main(int argc, char* argv[]) #endif // only implement communication with Authorization server if IS-10 is required -#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) if (nmos::experimental::fields::server_authorization(registry_model.settings)) { auto load_ca_certificates = registry_implementation.load_ca_certificates; registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_behaviour_thread(registry_model, authorization_state, load_ca_certificates, {}, {}, {}, {}, gate); }); registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_token_issuer_thread(registry_model, authorization_state, load_ca_certificates, gate); }); } -#endif // Open the API ports and start up registry management From 9daf6e9390541e753ed5a513c7f93bd7059e851f Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 22 May 2023 17:18:20 +0100 Subject: [PATCH 023/121] Update error log message --- Development/nmos/authorization.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp index f1fbcdd6c..bd33764ca 100644 --- a/Development/nmos/authorization.cpp +++ b/Development/nmos/authorization.cpp @@ -114,7 +114,7 @@ namespace nmos } else { - slog::log(gate, SLOG_FLF) << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; + slog::log(gate, SLOG_FLF) << "no " << utility::us2s(issuer->first.to_string()) << " public keys to validate access token"; } } catch (const no_matching_keys_exception& e) From 4a0416b9fceedaa2dd60832cc3a2f5968690b06c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 22 May 2023 17:22:55 +0100 Subject: [PATCH 024/121] Add `service_unavailable_retry_after` to settings --- Development/nmos-cpp-node/config.json | 8 ++++++++ Development/nmos-cpp-registry/config.json | 8 ++++++++ Development/nmos/api_utils.cpp | 11 ++++++----- Development/nmos/api_utils.h | 2 +- Development/nmos/events_ws_api.cpp | 3 ++- Development/nmos/settings.h | 8 ++++++++ Development/nmos/ws_api_utils.cpp | 3 ++- 7 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 5f65f90f7..c2a8f2030 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -355,5 +355,13 @@ // as it is because not all Authorization server can cope with URL with trailing dot, default to true //"no_trailing_dot_for_authorization_callback_uri": true, + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + //"service_unavailable_retry_after": 5, + "don't worry": "about trailing commas" } diff --git a/Development/nmos-cpp-registry/config.json b/Development/nmos-cpp-registry/config.json index b8b29f9b4..fedefd5ec 100644 --- a/Development/nmos-cpp-registry/config.json +++ b/Development/nmos-cpp-registry/config.json @@ -268,5 +268,13 @@ // server_authorization [registry, node]: whether server should use authorization to protect its APIs //"server_authorization": false, + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + //"service_unavailable_retry_after": 5, + "don't worry": "about trailing commas" } diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 8f51083df..6e0e06399 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -774,7 +774,7 @@ namespace nmos if (methods::OPTIONS == req.method()) return pplx::task_from_result(true); - const auto audience = with_read_lock(model.mutex, [&] { const auto& settings = model.settings; return nmos::get_host_name(settings); }); + const auto audience = with_read_lock(model.mutex, [&] { return nmos::get_host_name(model.settings); }); auto error = with_write_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &gate_] { return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, authorization_state.token_issuer, gate_); @@ -784,8 +784,9 @@ namespace nmos { // set error repsonse auto realm = web::http::get_host_port(req).first; - if (realm.empty()) { realm = nmos::get_host(model.settings); } - set_error_reply(res, realm, error); + if (realm.empty()) { realm = with_read_lock(model.mutex, [&] { return nmos::get_host(model.settings); }); } + const auto retry_after = with_read_lock(model.mutex, [&] { return nmos::experimental::fields::service_unavailable_retry_after(model.settings); }); + set_error_reply(res, realm, retry_after, error); // if error was deal to no matching keys, trigger a re-fetch to obtain public keys from the token issuer if (error.value == nmos::experimental::authorization_error::no_matching_keys) @@ -812,7 +813,7 @@ namespace nmos }; } - void set_error_reply(web::http::http_response& res, const utility::string_t& realm, const nmos::experimental::authorization_error& error) + void set_error_reply(web::http::http_response& res, const utility::string_t& realm, int retry_after, const nmos::experimental::authorization_error& error) { using namespace web::http; @@ -847,7 +848,7 @@ namespace nmos else if (error.value == nmos::experimental::authorization_error::no_matching_keys) { status_code = status_codes::ServiceUnavailable; - res.headers().add(web::http::header_names::retry_after, 5); //hmm, may be a shorter retry time? + res.headers().add(web::http::header_names::retry_after, retry_after); } nmos::set_error_reply(res, status_code, utility::s2us(error.message)); diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 9239c39e6..54e8d1a47 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -213,7 +213,7 @@ namespace nmos web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, slog::base_gate& gate); // set error response - void set_error_reply(web::http::http_response& res, const utility::string_t& realm, const nmos::experimental::authorization_error& error); + void set_error_reply(web::http::http_response& res, const utility::string_t& realm, int retry_after, const nmos::experimental::authorization_error& error); } } } diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index e1fb8f126..4facef72f 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -86,7 +86,8 @@ namespace nmos auto realm = web::http::get_host_port(req).first; if (realm.empty()) { realm = nmos::get_host(settings); } web::http::http_response res; - nmos::experimental::details::set_error_reply(res, realm, error); + const auto retry_after = nmos::experimental::fields::service_unavailable_retry_after(settings); + nmos::experimental::details::set_error_reply(res, realm, retry_after, error); req.reply(res); // if error was deal to no matching keys, trigger authorization_token_issuer_thread to fetch public keys from the token issuer diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index b952d742d..2110dc70a 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -435,6 +435,14 @@ namespace nmos // no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks // as it is because not all Authorization server can cope with URL with trailing dot, default to true const web::json::field_as_bool_or no_trailing_dot_for_authorization_callback_uri{ U("no_trailing_dot_for_authorization_callback_uri"), true}; + + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + const web::json::field_as_integer_or service_unavailable_retry_after{ U("service_unavailable_retry_after"), 5}; } } } diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp index 2c674188d..5f762de0d 100644 --- a/Development/nmos/ws_api_utils.cpp +++ b/Development/nmos/ws_api_utils.cpp @@ -28,7 +28,8 @@ namespace nmos auto realm = web::http::get_host_port(request).first; if (realm.empty()) { realm = nmos::get_host(settings); } web::http::http_response res; - nmos::experimental::details::set_error_reply(res, realm, error); + const auto retry_after = nmos::experimental::fields::service_unavailable_retry_after(settings); + nmos::experimental::details::set_error_reply(res, realm, retry_after, error); request.reply(res); // if error was deal to no matching keys, trigger authorization_token_issuer_thread to fetch public keys from the token issuer From 0e226087fbed9a6a4750340bb9d353ef0de22ed0 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 22 May 2023 17:29:30 +0100 Subject: [PATCH 025/121] Set client-name and retry-after for Node and Registry --- Sandbox/run_nmos_testing.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index f0068f799..2ca72df03 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -111,7 +111,9 @@ if [[ "${config_auth}" == "True" ]]; then echo "Running Auth tests" auth=true common_params+=",\ - \"server_authorization\":true\ + \"label\":\"nmos-cpp\",\ + \"server_authorization\":true,\ + \"service_unavailable_retry_after\":10\ " node_params=",\ \"client_authorization\":true,\ From 9c97886178e6983779659d0f807389aa7e70675a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 23 May 2023 09:49:58 +0100 Subject: [PATCH 026/121] Set Node and Registry label and up the retry-after value to 15s. Mac OS seems like taken longer on the HTTP. --- Sandbox/run_nmos_testing.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 2ca72df03..87dd27988 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -111,11 +111,14 @@ if [[ "${config_auth}" == "True" ]]; then echo "Running Auth tests" auth=true common_params+=",\ - \"label\":\"nmos-cpp\",\ \"server_authorization\":true,\ - \"service_unavailable_retry_after\":10\ + \"service_unavailable_retry_after\":15\ + " + registry_params=",\ + \"label\":\"nmos-cpp-registry\"\ " node_params=",\ + \"label\":\"nmos-cpp-node\",\ \"client_authorization\":true,\ \"authorization_flow\":\"client_credentials\",\ \"authorization_scopes\":[\"registration\"],\ @@ -207,7 +210,7 @@ do_run_test IS-08-02 $expected_disabled_IS_08_02 --host "${host}" "${host}" --po do_run_test IS-09-02 $expected_disabled_IS_09_02 --host "${host}" null --port 0 0 --version null v1.0 # Run Registry tests (leave Node running) -"${registry_command}" "{\"pri\":0,\"http_port\":8088 ${common_params}}" > ${results_dir}/registryoutput 2>&1 & +"${registry_command}" "{\"pri\":0,\"http_port\":8088 ${common_params} ${registry_params}}" > ${results_dir}/registryoutput 2>&1 & REGISTRY_PID=$! # short delay to give the Registry a chance to start up and the Node a chance to register before running the Registry test suite sleep 2 From c393c2c11e2e64b141e42de4b66d5a4fec9ce9fb Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 May 2023 11:35:48 +0100 Subject: [PATCH 027/121] Display the hosts contents while running windows setup --- .github/workflows/build-test.yml | 4 ++++ .github/workflows/src/build-setup.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d8c411354..e932083f3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -133,6 +133,8 @@ jobs: Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" + # display the hosts contents + type $env:WINDIR\System32\Drivers\Etc\Hosts # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg @@ -649,6 +651,8 @@ jobs: Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" + # display the hosts contents + type $env:WINDIR\System32\Drivers\Etc\Hosts # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index 2559b3092..a2941c428 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -41,6 +41,8 @@ Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" + # display the hosts contents + type $env:WINDIR\System32\Drivers\Etc\Hosts # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg From 8760cc0673f730060ea19583e270720bad5cc509 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 May 2023 17:55:30 +0100 Subject: [PATCH 028/121] Ping host and host_ip before executing BCP-003-01 tests --- Sandbox/run_nmos_testing.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 87dd27988..e358675c7 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -188,6 +188,8 @@ function do_run_test() { } if $secure; then + ping "${host}" -c 3 + ping "${host_ip}" -c 3 do_run_test BCP-003-01 $expected_disabled_BCP_003_01 --host "${host}" --port 1080 --version v1.0 fi From 9e8e1d4907ae003caf5a14380abc9157edd044b7 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 May 2023 18:22:00 +0100 Subject: [PATCH 029/121] Use -n instead of -c for number of packets to be sent for ping --- Sandbox/run_nmos_testing.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index e358675c7..033c409c9 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -188,8 +188,8 @@ function do_run_test() { } if $secure; then - ping "${host}" -c 3 - ping "${host_ip}" -c 3 + ping "${host}" -n 3 + ping "${host_ip}" -n 3 do_run_test BCP-003-01 $expected_disabled_BCP_003_01 --host "${host}" --port 1080 --version v1.0 fi From 8b156e10a3e443ab16bf5eb3c4bbffc5ecf1e436 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 12:07:38 +0100 Subject: [PATCH 030/121] Test testssl directly --- Sandbox/run_nmos_testing.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 033c409c9..415bfaa93 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -190,6 +190,7 @@ function do_run_test() { if $secure; then ping "${host}" -n 3 ping "${host_ip}" -n 3 + testssl/testssl.sh -p "${host}:1080" do_run_test BCP-003-01 $expected_disabled_BCP_003_01 --host "${host}" --port 1080 --version v1.0 fi From 40d7d8a7ef818190301a7901456c42dc8a7baf72 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 12:46:00 +0100 Subject: [PATCH 031/121] Install hexdump for testssl.sh --- .github/workflows/build-test.yml | 6 ++++++ .github/workflows/src/amwa-test.yml | 3 +++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e932083f3..edf45249d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -342,6 +342,9 @@ jobs: set -x root_dir=`pwd` + # Install hexdump + sudo apt-get install -f bsdmainutils + # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing @@ -860,6 +863,9 @@ jobs: set -x root_dir=`pwd` + # Install hexdump + sudo apt-get install -f bsdmainutils + # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index ac2ea972a..726d68f22 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -15,6 +15,9 @@ set -x root_dir=`pwd` + # Install hexdump + sudo apt-get install -f bsdmainutils + # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing From 7c483ff3a4a0c81760684e90926f4375cf8cec18 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 13:03:24 +0100 Subject: [PATCH 032/121] Remove sudo from install hexdump --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index edf45249d..ba942babf 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -343,7 +343,7 @@ jobs: root_dir=`pwd` # Install hexdump - sudo apt-get install -f bsdmainutils + apt-get install -f bsdmainutils # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git @@ -864,7 +864,7 @@ jobs: root_dir=`pwd` # Install hexdump - sudo apt-get install -f bsdmainutils + apt-get install -f bsdmainutils # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 726d68f22..2ede018a5 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -16,7 +16,7 @@ root_dir=`pwd` # Install hexdump - sudo apt-get install -f bsdmainutils + apt-get install -f bsdmainutils # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git From c9ccf2a8022c510159c41e43a9eda1acf1bc04a7 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 13:43:57 +0100 Subject: [PATCH 033/121] Change to use Vampire/setup-wsl@v2 --- .github/workflows/build-test.yml | 28 ++++++++-------------------- .github/workflows/src/amwa-test.yml | 14 ++++---------- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ba942babf..61d164e8c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -327,24 +327,18 @@ jobs: - name: install wsl if: runner.os == 'Windows' - run: | - & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx - Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip - Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 - cd ubuntu-1804 - .\ubuntu1804.exe install --root + uses: Vampire/setup-wsl@v2 + with: + distribution: Ubuntu-18.04 - name: AMWA test suite - shell: bash + shell: ${{ runner.os == 'Windows' && 'wsl-bash' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | set -x root_dir=`pwd` - # Install hexdump - apt-get install -f bsdmainutils - # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing @@ -848,24 +842,18 @@ jobs: - name: install wsl if: runner.os == 'Windows' - run: | - & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx - Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip - Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 - cd ubuntu-1804 - .\ubuntu1804.exe install --root + uses: Vampire/setup-wsl@v2 + with: + distribution: Ubuntu-18.04 - name: AMWA test suite - shell: bash + shell: ${{ runner.os == 'Windows' && 'wsl-bash' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | set -x root_dir=`pwd` - # Install hexdump - apt-get install -f bsdmainutils - # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 2ede018a5..743ef1139 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -1,23 +1,17 @@ - name: install wsl if: runner.os == 'Windows' - run: | - & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx - Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip - Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 - cd ubuntu-1804 - .\ubuntu1804.exe install --root + uses: Vampire/setup-wsl@v2 + with: + distribution: Ubuntu-18.04 - name: AMWA test suite - shell: bash + shell: ${{ runner.os == 'Windows' && 'wsl-bash' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | set -x root_dir=`pwd` - # Install hexdump - apt-get install -f bsdmainutils - # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing From fd6f4d1f0cfee4f7f40fc13a568a828692013160 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 13:47:40 +0100 Subject: [PATCH 034/121] Replace wsl-bash with wsl-bash {0} --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 61d164e8c..f8073b6e0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -332,7 +332,7 @@ jobs: distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ runner.os == 'Windows' && 'wsl-bash' || 'bash' }} + shell: ${{ runner.os == 'Windows' && 'wsl-bash {0}' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | @@ -847,7 +847,7 @@ jobs: distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ runner.os == 'Windows' && 'wsl-bash' || 'bash' }} + shell: ${{ runner.os == 'Windows' && 'wsl-bash {0}' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 743ef1139..caec5c620 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -5,7 +5,7 @@ distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ runner.os == 'Windows' && 'wsl-bash' || 'bash' }} + shell: ${{ runner.os == 'Windows' && 'wsl-bash {0}' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | From f7e0e2cd4821df6d4d31047b34535b7ebab083b9 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 14:05:50 +0100 Subject: [PATCH 035/121] Use of RUNNER_OS --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f8073b6e0..1a1fa3314 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -332,7 +332,7 @@ jobs: distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ runner.os == 'Windows' && 'wsl-bash {0}' || 'bash' }} + shell: ${{ env.RUNNER_OS == 'Windows' && 'wsl-bash {0}' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | @@ -847,7 +847,7 @@ jobs: distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ runner.os == 'Windows' && 'wsl-bash {0}' || 'bash' }} + shell: ${{ env.RUNNER_OS == 'Windows' && 'wsl-bash {0}' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index caec5c620..ba92132f4 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -5,7 +5,7 @@ distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ runner.os == 'Windows' && 'wsl-bash {0}' || 'bash' }} + shell: ${{ env.RUNNER_OS == 'Windows' && 'wsl-bash {0}' || 'bash' }} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | From 433a157896d5cd4a989477b2d4fd1b29b9fcead3 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 26 May 2023 14:16:17 +0100 Subject: [PATCH 036/121] Do wsl-bash test for windows --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1a1fa3314..f744a1311 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -332,7 +332,7 @@ jobs: distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ env.RUNNER_OS == 'Windows' && 'wsl-bash {0}' || 'bash' }} + shell: wsl-bash {0} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | @@ -847,7 +847,7 @@ jobs: distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ env.RUNNER_OS == 'Windows' && 'wsl-bash {0}' || 'bash' }} + shell: wsl-bash {0} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index ba92132f4..056851cd5 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -5,7 +5,7 @@ distribution: Ubuntu-18.04 - name: AMWA test suite - shell: ${{ env.RUNNER_OS == 'Windows' && 'wsl-bash {0}' || 'bash' }} + shell: wsl-bash {0} working-directory: ${{ env.RUNNER_WORKSPACE }} run: | From 6d0759d058b6ce703a17835557095eef8a64c6fe Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 30 May 2023 10:56:13 +0100 Subject: [PATCH 037/121] Install python & pip for WSL --- .github/workflows/build-test.yml | 16 ++++++++++++++++ .github/workflows/src/amwa-test.yml | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f744a1311..6ab36c18b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -382,6 +382,14 @@ jobs: # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" + else if [[ "${{ runner.os }}" == "Windows" ]]; then + # install python3 and pip + sudo apt install python3 python3-pip + # run test suite directly + sudo pip install --upgrade -r requirements.txt + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git + run_python="sudo python" else # run test suite directly pip install -r requirements.txt @@ -897,6 +905,14 @@ jobs: # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" + else if [[ "${{ runner.os }}" == "Windows" ]]; then + # install python3 and pip + sudo apt install python3 python3-pip + # run test suite directly + sudo pip install --upgrade -r requirements.txt + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git + run_python="sudo python" else # run test suite directly pip install -r requirements.txt diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 056851cd5..becb17948 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -55,6 +55,14 @@ # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" + else if [[ "${{ runner.os }}" == "Windows" ]]; then + # install python3 and pip + sudo apt install python3 python3-pip + # run test suite directly + sudo pip install --upgrade -r requirements.txt + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git + run_python="sudo python" else # run test suite directly pip install -r requirements.txt From 3cdf6ef78457312f67e8766874254fdee09b031f Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 30 May 2023 11:25:22 +0100 Subject: [PATCH 038/121] Fix elif typo --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6ab36c18b..978c9172e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -382,7 +382,7 @@ jobs: # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" - else if [[ "${{ runner.os }}" == "Windows" ]]; then + elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip sudo apt install python3 python3-pip # run test suite directly @@ -905,7 +905,7 @@ jobs: # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" - else if [[ "${{ runner.os }}" == "Windows" ]]; then + elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip sudo apt install python3 python3-pip # run test suite directly diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index becb17948..e48f78397 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -55,7 +55,7 @@ # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" - else if [[ "${{ runner.os }}" == "Windows" ]]; then + elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip sudo apt install python3 python3-pip # run test suite directly From c9d0f1c3886571fe20edf259fcc45aeda7faa46d Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 30 May 2023 12:03:34 +0100 Subject: [PATCH 039/121] Do apt-get update before install pip --- .github/workflows/build-test.yml | 2 ++ .github/workflows/src/amwa-test.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 978c9172e..dc083a190 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -384,6 +384,7 @@ jobs: run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip + sudo apt-get update sudo apt install python3 python3-pip # run test suite directly sudo pip install --upgrade -r requirements.txt @@ -907,6 +908,7 @@ jobs: run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip + sudo apt-get update sudo apt install python3 python3-pip # run test suite directly sudo pip install --upgrade -r requirements.txt diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index e48f78397..bd8bf9449 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -57,6 +57,7 @@ run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip + sudo apt-get update sudo apt install python3 python3-pip # run test suite directly sudo pip install --upgrade -r requirements.txt From 100c91801a4a50caf2180c43524b604b9790084c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 30 May 2023 12:28:06 +0100 Subject: [PATCH 040/121] Force install pip --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index dc083a190..26f886b5e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -385,7 +385,7 @@ jobs: elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip sudo apt-get update - sudo apt install python3 python3-pip + sudo apt-get -y install python3-pip # run test suite directly sudo pip install --upgrade -r requirements.txt # install SDPoker @@ -909,7 +909,7 @@ jobs: elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip sudo apt-get update - sudo apt install python3 python3-pip + sudo apt-get -y install python3-pip # run test suite directly sudo pip install --upgrade -r requirements.txt # install SDPoker diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index bd8bf9449..9ee1a6f62 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -58,7 +58,7 @@ elif [[ "${{ runner.os }}" == "Windows" ]]; then # install python3 and pip sudo apt-get update - sudo apt install python3 python3-pip + sudo apt-get -y install python3-pip # run test suite directly sudo pip install --upgrade -r requirements.txt # install SDPoker From 3b7124588091e5c58f225dde57ac97ec35db63f1 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 30 May 2023 17:51:35 +0100 Subject: [PATCH 041/121] pip install requirements.txt without upgrade --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 26f886b5e..a4e347ba6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -387,7 +387,7 @@ jobs: sudo apt-get update sudo apt-get -y install python3-pip # run test suite directly - sudo pip install --upgrade -r requirements.txt + pip install -r requirements.txt # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" @@ -911,7 +911,7 @@ jobs: sudo apt-get update sudo apt-get -y install python3-pip # run test suite directly - sudo pip install --upgrade -r requirements.txt + pip install -r requirements.txt # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 9ee1a6f62..278e8d494 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -60,7 +60,7 @@ sudo apt-get update sudo apt-get -y install python3-pip # run test suite directly - sudo pip install --upgrade -r requirements.txt + pip install -r requirements.txt # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" From bb7480ad6bac16c6bd5b2fe8ef2569af65e53d59 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 30 May 2023 17:52:38 +0100 Subject: [PATCH 042/121] Remove test code --- Sandbox/run_nmos_testing.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 415bfaa93..87dd27988 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -188,9 +188,6 @@ function do_run_test() { } if $secure; then - ping "${host}" -n 3 - ping "${host_ip}" -n 3 - testssl/testssl.sh -p "${host}:1080" do_run_test BCP-003-01 $expected_disabled_BCP_003_01 --host "${host}" --port 1080 --version v1.0 fi From b40c18267fb33fb3246dee72a5fe36c19d5e934a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 31 May 2023 00:12:51 +0100 Subject: [PATCH 043/121] Install pip for WSL --- .github/workflows/build-test.yml | 12 ++++++------ .github/workflows/src/amwa-test.yml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a4e347ba6..1ee9157f3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -383,9 +383,9 @@ jobs: npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then - # install python3 and pip - sudo apt-get update - sudo apt-get -y install python3-pip + # install pip + sudo apt update + sudo apt install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker @@ -907,9 +907,9 @@ jobs: npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then - # install python3 and pip - sudo apt-get update - sudo apt-get -y install python3-pip + # install pip + sudo apt update + sudo apt install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 278e8d494..40d311853 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -56,9 +56,9 @@ npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then - # install python3 and pip - sudo apt-get update - sudo apt-get -y install python3-pip + # install pip + sudo apt update + sudo apt install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker From 0308aafb5eac5be863d5b9edf33e00476a279bfe Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 31 May 2023 01:54:54 +0100 Subject: [PATCH 044/121] automate and unattended of apt-get --- .github/workflows/build-test.yml | 8 ++++++-- .github/workflows/src/amwa-test.yml | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1ee9157f3..37da041fd 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -385,7 +385,9 @@ jobs: elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip sudo apt update - sudo apt install python3-pip + export NEEDRESTART_MODE=a + export DEBIAN_FRONTEND=noninteractive + sudo apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker @@ -909,7 +911,9 @@ jobs: elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip sudo apt update - sudo apt install python3-pip + export NEEDRESTART_MODE=a + export DEBIAN_FRONTEND=noninteractive + sudo apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 40d311853..ad921f3f4 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -58,7 +58,9 @@ elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip sudo apt update - sudo apt install python3-pip + export NEEDRESTART_MODE=a + export DEBIAN_FRONTEND=noninteractive + sudo apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker From 100999cea7ec3cf101e2729d4d0feb069b62d28a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 31 May 2023 03:23:27 +0100 Subject: [PATCH 045/121] variables are set using sudo not export --- .github/workflows/build-test.yml | 8 ++++---- .github/workflows/src/amwa-test.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 37da041fd..5b3a5dbf1 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -384,9 +384,9 @@ jobs: run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip + sudo NEEDRESTART_MODE=a + sudo DEBIAN_FRONTEND=noninteractive sudo apt update - export NEEDRESTART_MODE=a - export DEBIAN_FRONTEND=noninteractive sudo apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt @@ -910,9 +910,9 @@ jobs: run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip + sudo NEEDRESTART_MODE=a + sudo DEBIAN_FRONTEND=noninteractive sudo apt update - export NEEDRESTART_MODE=a - export DEBIAN_FRONTEND=noninteractive sudo apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index ad921f3f4..7e1e1cab2 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -57,9 +57,9 @@ run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip + sudo NEEDRESTART_MODE=a + sudo DEBIAN_FRONTEND=noninteractive sudo apt update - export NEEDRESTART_MODE=a - export DEBIAN_FRONTEND=noninteractive sudo apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt From f7ce3830330a4bb35c53c9363ec6f6fa356e4dce Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 31 May 2023 09:23:38 +0100 Subject: [PATCH 046/121] Set zero interaction while installing pip --- .github/workflows/build-test.yml | 8 ++------ .github/workflows/src/amwa-test.yml | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5b3a5dbf1..90b69b6cf 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -384,10 +384,8 @@ jobs: run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip - sudo NEEDRESTART_MODE=a - sudo DEBIAN_FRONTEND=noninteractive sudo apt update - sudo apt-get -y install python3-pip + sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker @@ -910,10 +908,8 @@ jobs: run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip - sudo NEEDRESTART_MODE=a - sudo DEBIAN_FRONTEND=noninteractive sudo apt update - sudo apt-get -y install python3-pip + sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 7e1e1cab2..0cb388635 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -57,10 +57,8 @@ run_python="sudo python" elif [[ "${{ runner.os }}" == "Windows" ]]; then # install pip - sudo NEEDRESTART_MODE=a - sudo DEBIAN_FRONTEND=noninteractive sudo apt update - sudo apt-get -y install python3-pip + sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-pip # run test suite directly pip install -r requirements.txt # install SDPoker From abbb9b4c4a84a059a8909da4a3aa3db2c1c43e11 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 31 May 2023 11:38:54 +0100 Subject: [PATCH 047/121] Return using curl to download and install WSL --- .github/workflows/build-test.yml | 40 ++++++++++------------------- .github/workflows/src/amwa-test.yml | 20 +++++---------- 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 90b69b6cf..e932083f3 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -327,12 +327,15 @@ jobs: - name: install wsl if: runner.os == 'Windows' - uses: Vampire/setup-wsl@v2 - with: - distribution: Ubuntu-18.04 + run: | + & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx + Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip + Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 + cd ubuntu-1804 + .\ubuntu1804.exe install --root - name: AMWA test suite - shell: wsl-bash {0} + shell: bash working-directory: ${{ env.RUNNER_WORKSPACE }} run: | @@ -382,15 +385,6 @@ jobs: # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" - elif [[ "${{ runner.os }}" == "Windows" ]]; then - # install pip - sudo apt update - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-pip - # run test suite directly - pip install -r requirements.txt - # install SDPoker - npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git - run_python="sudo python" else # run test suite directly pip install -r requirements.txt @@ -851,12 +845,15 @@ jobs: - name: install wsl if: runner.os == 'Windows' - uses: Vampire/setup-wsl@v2 - with: - distribution: Ubuntu-18.04 + run: | + & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx + Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip + Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 + cd ubuntu-1804 + .\ubuntu1804.exe install --root - name: AMWA test suite - shell: wsl-bash {0} + shell: bash working-directory: ${{ env.RUNNER_WORKSPACE }} run: | @@ -906,15 +903,6 @@ jobs: # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" - elif [[ "${{ runner.os }}" == "Windows" ]]; then - # install pip - sudo apt update - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-pip - # run test suite directly - pip install -r requirements.txt - # install SDPoker - npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git - run_python="sudo python" else # run test suite directly pip install -r requirements.txt diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 0cb388635..ac2ea972a 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -1,11 +1,14 @@ - name: install wsl if: runner.os == 'Windows' - uses: Vampire/setup-wsl@v2 - with: - distribution: Ubuntu-18.04 + run: | + & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx + Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip + Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 + cd ubuntu-1804 + .\ubuntu1804.exe install --root - name: AMWA test suite - shell: wsl-bash {0} + shell: bash working-directory: ${{ env.RUNNER_WORKSPACE }} run: | @@ -55,15 +58,6 @@ # install SDPoker npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="sudo python" - elif [[ "${{ runner.os }}" == "Windows" ]]; then - # install pip - sudo apt update - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install python3-pip - # run test suite directly - pip install -r requirements.txt - # install SDPoker - npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git - run_python="sudo python" else # run test suite directly pip install -r requirements.txt From 598c60b0d603dcc6afd850882ef3b2fe093f31aa Mon Sep 17 00:00:00 2001 From: Simon Lo Date: Fri, 2 Jun 2023 11:51:14 +0100 Subject: [PATCH 048/121] Update build-test.yml Show wsl version before running AMWA testing --- .github/workflows/build-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ed8737899..c1377b9b1 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -338,6 +338,7 @@ jobs: run: | set -x + wsl --version root_dir=`pwd` # Install AMWA NMOS Testing Tool @@ -847,6 +848,7 @@ jobs: run: | set -x + wsl --version root_dir=`pwd` # Install AMWA NMOS Testing Tool From 32e857e4cda66950f8cca060fd8c5c5e1f9e6797 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 2 Jun 2023 14:15:25 +0100 Subject: [PATCH 049/121] Test bash location before and after wsl installation --- .github/workflows/build-test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 83bc19cd9..9943c2170 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -332,7 +332,9 @@ jobs: Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 cd ubuntu-1804 + where.exe bash .\ubuntu1804.exe install --root + where.exe bash - name: AMWA test suite shell: bash @@ -340,7 +342,6 @@ jobs: run: | set -x - wsl --version root_dir=`pwd` # Install AMWA NMOS Testing Tool @@ -851,7 +852,9 @@ jobs: Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 cd ubuntu-1804 + where.exe bash .\ubuntu1804.exe install --root + where.exe bash - name: AMWA test suite shell: bash @@ -859,7 +862,6 @@ jobs: run: | set -x - wsl --version root_dir=`pwd` # Install AMWA NMOS Testing Tool From 4653a4d3bf9dc7801ee6dba7c20e854d96cd9c41 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 5 Jun 2023 11:55:01 +0100 Subject: [PATCH 050/121] Check testssl version --- .github/workflows/build-test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9943c2170..427b44c0b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -365,6 +365,9 @@ jobs: curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null cd .. + # Check testssl version + bash testssl/testssl.sh -v + # Create output directories mkdir results mkdir badges From 94b837a4a649761886eb722c8c0abbc0aba2aaed Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 5 Jun 2023 16:54:17 +0100 Subject: [PATCH 051/121] Set TEST_SSL_BASH for testing --- .github/workflows/build-test.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 427b44c0b..c22d4e43e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -359,15 +359,13 @@ jobs: hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + printf 'CONFIG.TEST_SSL_BASH = C:\Windows\System32\bash.exe\n' >> nmostesting/UserConfig.py # Download testssl cd testssl curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null cd .. - - # Check testssl version - bash testssl/testssl.sh -v - + # Create output directories mkdir results mkdir badges From 98fddf8031e828d32f92a30d768db8bf80e9b7b4 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 5 Jun 2023 17:37:06 +0100 Subject: [PATCH 052/121] Set TEST_SSL_BASH for windows --- .github/workflows/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index c22d4e43e..6f404c3c6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -359,7 +359,7 @@ jobs: hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - printf 'CONFIG.TEST_SSL_BASH = C:\Windows\System32\bash.exe\n' >> nmostesting/UserConfig.py + printf 'CONFIG.TEST_SSL_BASH = "C:\Windows\System32\bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl From 18725694e4141faa147cb27f9e90edaf0122ddfb Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 6 Jun 2023 10:34:51 +0100 Subject: [PATCH 053/121] Update TEST_SSL_BASH for windows --- .github/workflows/build-test.yml | 9 +++------ .github/workflows/src/amwa-test.yml | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6f404c3c6..2976cb044 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -332,9 +332,7 @@ jobs: Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 cd ubuntu-1804 - where.exe bash .\ubuntu1804.exe install --root - where.exe bash - name: AMWA test suite shell: bash @@ -359,13 +357,13 @@ jobs: hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - printf 'CONFIG.TEST_SSL_BASH = "C:\Windows\System32\bash.exe"\n' >> nmostesting/UserConfig.py + printf 'CONFIG.TEST_SSL_BASH = "C:/Windows/System32/bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null cd .. - + # Create output directories mkdir results mkdir badges @@ -853,9 +851,7 @@ jobs: Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 cd ubuntu-1804 - where.exe bash .\ubuntu1804.exe install --root - where.exe bash - name: AMWA test suite shell: bash @@ -880,6 +876,7 @@ jobs: hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + printf 'CONFIG.TEST_SSL_BASH = "C:/Windows/System32/bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index ac2ea972a..069b5844b 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -30,6 +30,7 @@ hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + printf 'CONFIG.TEST_SSL_BASH = "C:/Windows/System32/bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl From 5ddce8f7e6af8963f22b6d7a602d3895754a6910 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 7 Jun 2023 18:55:33 +0100 Subject: [PATCH 054/121] Check dig installed --- .github/workflows/build-test.yml | 8 ++++++-- .github/workflows/src/amwa-test.yml | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2976cb044..9f2bc6182 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -357,7 +357,6 @@ jobs: hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - printf 'CONFIG.TEST_SSL_BASH = "C:/Windows/System32/bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl @@ -396,6 +395,9 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then + # check dig + dig -v + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx @@ -876,7 +878,6 @@ jobs: hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - printf 'CONFIG.TEST_SSL_BASH = "C:/Windows/System32/bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl @@ -915,6 +916,9 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then + # check dig + dig -v + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 069b5844b..52506058a 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -30,7 +30,6 @@ hostname=api.testsuite.nmos.tv fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - printf 'CONFIG.TEST_SSL_BASH = "C:/Windows/System32/bash.exe"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl @@ -69,6 +68,9 @@ pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then + # check dig + dig -v + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx From 3364d8ca9e9262ebd05bb17976f40430f0bd00ca Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 8 Jun 2023 02:51:44 +0100 Subject: [PATCH 055/121] Install dig for windows --- .github/workflows/build-test.yml | 12 ++++++++---- .github/workflows/src/amwa-test.yml | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9f2bc6182..01ef6e61d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -395,9 +395,11 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # check dig - dig -v + # install dig + sudo apt update + sudo DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx @@ -916,9 +918,11 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # check dig - dig -v + # install dig + sudo apt update + sudo DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 52506058a..62ba82103 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -68,9 +68,11 @@ pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # check dig - dig -v + # install dig + sudo apt update + sudo DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx From 36abd085371808651e56a7121ab82b92e682affc Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 8 Jun 2023 03:29:55 +0100 Subject: [PATCH 056/121] Remove sudo for windows --- .github/workflows/build-test.yml | 8 ++++---- .github/workflows/src/amwa-test.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 01ef6e61d..18806b02f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -396,8 +396,8 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - sudo apt update - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + apt update + DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem @@ -919,8 +919,8 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - sudo apt update - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + apt update + DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 62ba82103..e6cef1798 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -69,8 +69,8 @@ if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - sudo apt update - sudo DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + apt update + DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem From e48063721cc2b26ba72308c68892c3e267c967d3 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 8 Jun 2023 08:31:37 +0100 Subject: [PATCH 057/121] Use bash to execute apt update --- .github/workflows/build-test.yml | 8 ++++---- .github/workflows/src/amwa-test.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 18806b02f..cefba0728 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -396,8 +396,8 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - apt update - DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + bash apt update + bash DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem @@ -919,8 +919,8 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - apt update - DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + bash apt update + bash DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index e6cef1798..c3526458f 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -69,8 +69,8 @@ if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - apt update - DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + bash apt update + bash DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem From c7ce27133d1ea1d53c43aa97da8772d89a5ab1e7 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 8 Jun 2023 15:17:13 +0100 Subject: [PATCH 058/121] Install dig for windows runner --- .github/workflows/build-test.yml | 6 ++---- .github/workflows/src/amwa-test.yml | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index cefba0728..0cffa7e4e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -396,8 +396,7 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - bash apt update - bash DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + bash apt-get install dig # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem @@ -919,8 +918,7 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - bash apt update - bash DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + bash apt-get install dig # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index c3526458f..4e39a2dda 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -69,8 +69,7 @@ if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - bash apt update - bash DEBIAN_FRONTEND=noninteractive apt-get -y install dnsutils + bash apt-get install dig # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem From f8105e98e2f6646474341691609c097559543ffe Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 8 Jun 2023 15:54:07 +0100 Subject: [PATCH 059/121] Use wsl bash to install dig for windows runner --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 0cffa7e4e..7d92d6d73 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -396,7 +396,7 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - bash apt-get install dig + wsl bash -c "sudo apt-get install -y dnsutils" # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem @@ -918,7 +918,7 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - bash apt-get install dig + wsl bash -c "sudo apt-get install -y dnsutils" # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 4e39a2dda..2671f2aee 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -69,7 +69,7 @@ if [[ "${{ runner.os }}" == "Windows" ]]; then # install dig - bash apt-get install dig + wsl bash -c "sudo apt-get install -y dnsutils" # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem From 364fd37d50695cbfc8ddd22a0473dc04c06b907b Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 9 Jun 2023 12:57:53 +0100 Subject: [PATCH 060/121] No need to install dig for the windows runner, it has already installed in wsl --- .github/workflows/build-test.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 7d92d6d73..a43f0f1f9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -395,9 +395,6 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # install dig - wsl bash -c "sudo apt-get install -y dnsutils" - # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem @@ -917,9 +914,6 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # install dig - wsl bash -c "sudo apt-get install -y dnsutils" - # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem From a4584bc736087062fd99e9b98643bb930b56c1b1 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 9 Jun 2023 12:58:50 +0100 Subject: [PATCH 061/121] No need to install dig to the windows runner, it has already installed in wsl --- .github/workflows/src/amwa-test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 2671f2aee..f066ebb07 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -68,9 +68,6 @@ pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # install dig - wsl bash -c "sudo apt-get install -y dnsutils" - # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem From ead26ef5a59b74cee8c5adbd027003aaf210f29c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 9 Jun 2023 13:02:26 +0100 Subject: [PATCH 062/121] Add test to verify whether dig is working immediately after nmos-cpp-node started --- Sandbox/run_nmos_testing.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 87dd27988..400824668 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -146,6 +146,19 @@ fi "${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params} ${node_params}}" > ${results_dir}/nodeoutput 2>&1 & NODE_PID=$! + +if [[ "${config_dns_sd_mode}" == "multicast" ]]; then + dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + sleep 1 + dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + sleep 2 + dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + sleep 4 + dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' +fi + + + function do_run_test() { suite=$1 echo "Running $suite" From b56a08fff3378bd04a2c5dc26153adc00ec8d057 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 9 Jun 2023 16:30:52 +0100 Subject: [PATCH 063/121] Show dig results before running nmos-testing --- Sandbox/run_nmos_testing.sh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 400824668..c54ffb090 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -148,13 +148,26 @@ NODE_PID=$! if [[ "${config_dns_sd_mode}" == "multicast" ]]; then - dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "1 $IP4" sleep 1 - dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "2 $IP4" sleep 2 - dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "3 $IP4" sleep 4 - dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d' + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "4 $IP4" + sleep 8 + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "5 $IP4" + sleep 16 + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "6 $IP4" + sleep 32 + IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') + echo "7 $IP4" fi From 9eef4407ee3dfe1be7c9cfd36e965720b71293f6 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 12 Jun 2023 16:00:26 +0100 Subject: [PATCH 064/121] Override the original BCP-003-01 test to include the ip address (--ip) for windows runner --- .github/workflows/build-test.yml | 70 ++++++++++++++++------------- .github/workflows/src/amwa-test.yml | 35 ++++++++------- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a43f0f1f9..ad2c47e43 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -395,21 +395,26 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # install certificates - certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem - certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx - - # RSA - netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - - # RSA - netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + fi + + # install certificates + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem + certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx + + # RSA + netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + # RSA + netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" fi if [[ "${{ runner.os }}" == "macOS" ]]; then @@ -914,21 +919,26 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # install certificates - certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem - certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx - - # RSA - netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - - # RSA - netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + fi + + # install certificates + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem + certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx + + # RSA + netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + # RSA + netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" fi if [[ "${{ runner.os }}" == "macOS" ]]; then diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index f066ebb07..4e163188e 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -68,21 +68,26 @@ pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # install certificates - certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem - certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx - - # RSA - netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - - # RSA - netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + fi + + # install certificates + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem + certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx + + # RSA + netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + # RSA + netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" fi if [[ "${{ runner.os }}" == "macOS" ]]; then From 76fed9ff7117bd6e72c5cfd06fd6cda652796a97 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 12 Jun 2023 20:46:59 +0100 Subject: [PATCH 065/121] Show the modified nmostesting/suites/BCP00301Test.py contents --- .github/workflows/build-test.yml | 12 ++++++++---- .github/workflows/src/amwa-test.yml | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ad2c47e43..dd809da0c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -396,8 +396,10 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + + cat nmostesting/suites/BCP00301Test.py fi # install certificates @@ -920,8 +922,10 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + + cat nmostesting/suites/BCP00301Test.py fi # install certificates diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 4e163188e..0d515900e 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -69,8 +69,10 @@ if [[ "${{ runner.os }}" == "Windows" ]]; then if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + + cat nmostesting/suites/BCP00301Test.py fi # install certificates From db585ec0d503f178bd6e358bd6ed1929f36ef8cb Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 13 Jun 2023 10:54:55 +0100 Subject: [PATCH 066/121] Run tests with AUTH switched off --- .github/workflows/build-test.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index dd809da0c..c07675531 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -347,16 +347,17 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py + # printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token - if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - hostname=nmos-api.local - else - hostname=api.testsuite.nmos.tv - fi - printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + # if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + # hostname=nmos-api.local + # else + # hostname=api.testsuite.nmos.tv + # fi + # printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl @@ -395,12 +396,11 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py - - cat nmostesting/suites/BCP00301Test.py - fi + # if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + # # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + # printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # cat nmostesting/suites/BCP00301Test.py + # fi # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem From f2f51708ebb6e671ec5ec92dfc26ed0160c5a529 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 29 Sep 2023 18:19:55 +0100 Subject: [PATCH 067/121] lock shared resource while running public keys requests, and code tidy up --- Development/nmos/api_utils.cpp | 3 +- Development/nmos/authorization_behaviour.cpp | 22 +- Development/nmos/authorization_handlers.cpp | 2 - Development/nmos/authorization_operation.cpp | 197 ++++++++---------- .../nmos/authorization_redirect_api.cpp | 6 +- Development/nmos/authorization_state.cpp | 29 ++- 6 files changed, 119 insertions(+), 140 deletions(-) diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 6e0e06399..62171e014 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -777,6 +777,7 @@ namespace nmos const auto audience = with_read_lock(model.mutex, [&] { return nmos::get_host_name(model.settings); }); auto error = with_write_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &gate_] { + // note: the validate_authorization will update the authorization_state.token_issuer, i.e. using with_write_lock to protected it return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, authorization_state.token_issuer, gate_); }); @@ -788,7 +789,7 @@ namespace nmos const auto retry_after = with_read_lock(model.mutex, [&] { return nmos::experimental::fields::service_unavailable_retry_after(model.settings); }); set_error_reply(res, realm, retry_after, error); - // if error was deal to no matching keys, trigger a re-fetch to obtain public keys from the token issuer + // if error was deal to no matching keys, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) if (error.value == nmos::experimental::authorization_error::no_matching_keys) { slog::log(gate, SLOG_FLF) << "Authorization warning: " << error.message; diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp index 41ebf13de..6d45e71df 100644 --- a/Development/nmos/authorization_behaviour.cpp +++ b/Development/nmos/authorization_behaviour.cpp @@ -83,20 +83,17 @@ namespace nmos // load authorization clients metadata to cache if (load_authorization_clients) { - with_write_lock(authorization_state.mutex, [&] + const auto auth_clients = load_authorization_clients(); + + if (!auth_clients.is_null() && auth_clients.is_array()) { - const auto auth_clients = load_authorization_clients(); + slog::log(gate, SLOG_FLF) << "Retrieved authorization clients: " << utility::us2s(auth_clients.serialize()) << " from non-volatile memory"; - if (!auth_clients.is_null() && auth_clients.is_array()) + for (const auto& auth_client : auth_clients.as_array()) { - slog::log(gate, SLOG_FLF) << "Retrieved authorization clients: " << utility::us2s(auth_clients.serialize()) << " from non-volatile memory"; - - for (const auto& auth_client : auth_clients.as_array()) - { - nmos::experimental::update_client_metadata(authorization_state, auth_client.at(nmos::experimental::fields::authorization_server_uri).as_string(), nmos::experimental::fields::client_metadata(auth_client)); - } + nmos::experimental::update_client_metadata(authorization_state, auth_client.at(nmos::experimental::fields::authorization_server_uri).as_string(), nmos::experimental::fields::client_metadata(auth_client)); } - }); + } } bool authorization_service_error{ false }; @@ -138,8 +135,8 @@ namespace nmos case request_authorization_server_metadata: if (details::request_authorization_server_metadata(model, authorization_state, authorization_service_error, load_ca_certificates, gate)) { - // reterive client metadat from cache - const auto client_metadata = with_read_lock(authorization_state.mutex, [&] { return nmos::experimental::get_client_metadata(authorization_state); }); + // reterive client metadata from cache + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); // is it not a scopeless client (where scopeless client doesn't access any protected APIs, i.e. doesn't require to register to Authorization server) if (with_read_lock(model.mutex, [&] { return details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)).size(); })) @@ -201,7 +198,6 @@ namespace nmos else { // remove client metadata from cache - auto lock = authorization_state.read_lock(); nmos::experimental::erase_client_metadata(authorization_state); // client not known by the Authorization server, trigger client registration process diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index e49fcb066..da498459b 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -245,8 +245,6 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Make OAuth 2.0 config using bearer_token cache"; - auto lock = authorization_state.read_lock(); - const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); const auto client_metadata = get_client_metadata(authorization_state); diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index b26e98beb..39b0df5e0 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -279,7 +279,7 @@ namespace nmos // make an asynchronously GET request on the Authorization API to fetch authorization server metadata pplx::task request_authorization_server_metadata(web::http::client::http_client client, const std::set& scopes, const std::set& grants, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting authorization server metadata"; + slog::log(gate, SLOG_FLF) << "Requesting authorization server metadata at " << client.base_uri().to_string(); using namespace web::http; @@ -392,7 +392,7 @@ namespace nmos // e.g. curl -X POST "https://authorization.server.example.com/register" -H "Content-Type: application/json" -d "{\"redirect_uris\": [\"https://client.example.com/callback/\"],\"client_name\": \"My Example Client\",\"client_uri\": \"https://client.example.com/details.html\",\"token_endpoint_auth_method\": \"client_secret_basic\",\"response_types\": [\"code\",\"token\"],\"scope\": \"registration query node connection\",\"grant_types\": [\"authorization_code\",\"refresh_token\",\"client_credentials\"],\"token_endpoint_auth_method\": \"client_secret_basic\"}" pplx::task request_client_registration(web::http::client::http_client client, const utility::string_t& client_name, const std::vector& redirect_uris, const web::uri& client_uri, const std::set& response_types, const std::set& scopes, const std::set& grants, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const web::json::value& jwk, const web::uri& jwks_uri, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting authorization client registration"; + slog::log(gate, SLOG_FLF) << "Requesting authorization client registration at " << client.base_uri().to_string(); using namespace web; using namespace web::http; @@ -474,7 +474,7 @@ namespace nmos } } - slog::log(gate, SLOG_FLF) << "Request to register client metadata: " << utility::us2s(metadata.serialize()); + slog::log(gate, SLOG_FLF) << "Request to register client metadata: " << utility::us2s(metadata.serialize()) << " at " << client.base_uri().to_string(); return nmos::api_request(client, methods::POST, {}, metadata, gate, token).then([=, &gate](pplx::task response_task) { @@ -509,7 +509,7 @@ namespace nmos // make an asynchronously GET request on the Authorization API to fetch the authorization JSON Web Keys (public keys) pplx::task request_jwks(web::http::client::http_client client, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting authorization jwks"; + slog::log(gate, SLOG_FLF) << "Requesting authorization jwks at " << client.base_uri().to_string(); using namespace web::http; using oauth2::experimental::oauth2_exception; @@ -557,7 +557,7 @@ namespace nmos // make an asynchronously GET request on the OpenID Connect Authorization API to fetch the client metdadata pplx::task request_client_metadata_from_openid_connect(web::http::client::http_client client, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata"; + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata at " << client.base_uri().to_string(); using namespace web::http; @@ -596,7 +596,7 @@ namespace nmos // see https://medium.com/@software_factotum/pkce-public-clients-and-refresh-token-d1faa4ef6965#:~:text=Refresh%20Token%20are%20credentials%20that,application%20needs%20additional%20access%20tokens.&text=Authorization%20Server%20may%20issue%20a,Client%20it%20was%20issued%20to. pplx::task request_token(web::http::client::http_client client, const nmos::api_version& version, web::uri_builder& request_body_ub, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token"; + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token at " << client.base_uri().to_string(); using namespace web::http; using oauth2::details::oauth2_strings; @@ -669,7 +669,7 @@ namespace nmos // make an asynchronously POST request on the Authorization API to fetch the bearer token using client_credentials grant pplx::task request_token_from_client_credentials(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant"; + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant at " << client.base_uri().to_string(); using web::http::oauth2::details::oauth2_strings; @@ -682,7 +682,7 @@ namespace nmos // make an asynchronously POST request on the Authorization API to fetch the bearer token using client_credentials grant with private_key_jwt for client authentication pplx::task request_token_from_client_credentials_using_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant with private_key_jwt"; + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant with private_key_jwt at " << client.base_uri().to_string(); using web::http::oauth2::details::oauth2_strings; @@ -712,7 +712,7 @@ namespace nmos // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with code_verifier: " << utility::us2s(code_verifier); + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with code_verifier: " << utility::us2s(code_verifier) << " at " << client.base_uri().to_string(); auto ub = make_request_token_base_query(code, redirect_uri, code_verifier); @@ -722,7 +722,7 @@ namespace nmos // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication pplx::task request_token_from_authorization_code_with_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with private_key_jwt and code_verifier: " << utility::us2s(code_verifier) << " and client_assertion: " << utility::us2s(client_assertion); + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with private_key_jwt and code_verifier: " << utility::us2s(code_verifier) << " and client_assertion: " << utility::us2s(client_assertion) << " at " << client.base_uri().to_string(); auto ub = make_request_token_base_query(code, redirect_uri, code_verifier); // use private_key_jwt client authentication @@ -746,13 +746,7 @@ namespace nmos // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant pplx::task request_token_from_refresh_token(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& refresh_token, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant"; - - //using web::http::oauth2::details::oauth2_strings; - - //web::uri_builder ub; - //ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); - //ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant at " << client.base_uri().to_string(); auto ub = make_request_token_base_query(refresh_token); @@ -762,7 +756,7 @@ namespace nmos // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant with private_key_jwt for client authentication pplx::task request_token_from_refresh_token_using_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& refresh_token, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { - slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant with private_key_jwt"; + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant with private_key_jwt at " << client.base_uri().to_string(); using web::http::oauth2::details::oauth2_strings; @@ -895,19 +889,12 @@ namespace nmos , immediate(immediate) {} }; - // task to continous fetching bearer token in a time interval - pplx::task request_token(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, token_shared_state& token_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + // task to continuously fetch the bearer token on a time interval until failure or cancellation + pplx::task do_token_requests(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, token_shared_state& token_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { const auto access_token_refresh_interval = nmos::experimental::fields::access_token_refresh_interval(model.settings); - - web::json::value authorization_server_metadata{}; - web::json::value client_metadata{}; - with_read_lock(authorization_state.mutex, [&] - { - authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); - client_metadata = nmos::experimental::get_client_metadata(authorization_state); - }); - + const auto authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); const auto client_id = nmos::experimental::fields::client_id(client_metadata); const auto client_secret = client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""); const auto scope = nmos::experimental::fields::scope(client_metadata); @@ -931,7 +918,7 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token for about " << fetch_interval.count() << " seconds"; auto fetch_time = std::chrono::steady_clock::now(); - return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &model , &token_state, &gate]() + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &model, &token_state, &gate]() { // create client assertion using private key jwt utility::string_t client_assertion; @@ -1032,8 +1019,8 @@ namespace nmos }); } - // task to fetch public keys - pplx::task request_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, pubkeys_shared_state& pubkeys_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + // task to continuously fetch the authorization server public keys on a time interval until failure or cancellation + pplx::task do_public_keys_requests(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, pubkeys_shared_state& pubkeys_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { const auto fetch_interval_min(nmos::experimental::fields::fetch_authorization_public_keys_interval_min(model.settings)); const auto fetch_interval_max(nmos::experimental::fields::fetch_authorization_public_keys_interval_max(model.settings)); @@ -1050,12 +1037,12 @@ namespace nmos { fetch_interval = std::chrono::seconds((int)(std::uniform_real_distribution<>(fetch_interval_min, fetch_interval_max)(pubkeys_state.engine))); } - pubkeys_state.immediate = false; + nmos::with_write_lock(authorization_state.mutex, [&] { pubkeys_state.immediate = false; }); slog::log(gate, SLOG_FLF) << "Requesting authorization public keys (jwks) for about " << fetch_interval.count() << " seconds"; auto fetch_time = std::chrono::steady_clock::now(); - return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() mutable + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() //mutable { auto lock = authorization_state.read_lock(); @@ -1063,9 +1050,17 @@ namespace nmos }).then([&authorization_state, &pubkeys_state, &gate](web::json::value jwks_) { - auto lock = authorization_state.write_lock(); + web::uri issuer; + bool one_shot{ false }; + nmos::api_version auth_version; + nmos::with_read_lock(authorization_state.mutex, [&] + { + issuer = pubkeys_state.issuer; + one_shot = pubkeys_state.one_shot; + auth_version = pubkeys_state.version; + }); - const auto& jwks = nmos::experimental::get_jwks(authorization_state, pubkeys_state.issuer); + const auto jwks = nmos::experimental::get_jwks(authorization_state, issuer); // are changes found in new set of jwks? if(jwks != jwks_) @@ -1085,35 +1080,34 @@ namespace nmos } catch (const jwk_exception& e) { - slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(pubkeys_state.issuer.to_string()) << " JWK error: " << e.what(); + slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(issuer.to_string()) << " JWK error: " << e.what(); } } // update jwks and jwt validator cache if (pems.as_array().size()) { - const auto auth_version = pubkeys_state.version; - nmos::experimental::update_jwks(authorization_state, pubkeys_state.issuer, jwks_, nmos::experimental::jwt_validator(pems, [auth_version](const web::json::value& payload) + nmos::experimental::update_jwks(authorization_state, issuer, jwks_, nmos::experimental::jwt_validator(pems, [auth_version](const web::json::value& payload) { // validate access token payload JSON authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(auth_version)); // may throw json_exception })); - slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys from " << utility::us2s(pubkeys_state.issuer.to_string()); + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys from " << utility::us2s(issuer.to_string()); } else { - nmos::experimental::erase_jwks(authorization_state, pubkeys_state.issuer); + nmos::experimental::erase_jwks(authorization_state, issuer); - slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list from " << utility::us2s(pubkeys_state.issuer.to_string()); + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list from " << utility::us2s(issuer.to_string()); } } else { - slog::log(gate, SLOG_FLF) << "No public keys changes found from " << utility::us2s(pubkeys_state.issuer.to_string()); + slog::log(gate, SLOG_FLF) << "No public keys changes found from " << utility::us2s(issuer.to_string()); } - return !pubkeys_state.one_shot; + return !one_shot; }); }).then([&](pplx::task finally) { @@ -1123,7 +1117,7 @@ namespace nmos { finally.get(); - pubkeys_state.received = true; + nmos::with_write_lock(authorization_state.mutex, [&] { pubkeys_state.received = true; }); authorization_service_error = false; } catch (const web::http::http_exception& e) @@ -1180,16 +1174,10 @@ namespace nmos pplx::cancellation_token_source cancellation_source; pplx::task request = pplx::task_from_result(); - std::set scopes; - std::set grants; - web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method; - with_read_lock(authorization_state.mutex, [&] - { - const auto& client_metadata = nmos::experimental::get_client_metadata(authorization_state); - scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); - grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); - token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); - }); + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + const auto grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); + const auto token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); for (;;) { @@ -1223,10 +1211,11 @@ namespace nmos const auto auth_version = service.first.first; request = details::request_authorization_server_metadata(*client, scopes, grants, token_endpoint_auth_method, auth_version, gate, token).then([&authorization_state](web::json::value metadata) { - auto lock = authorization_state.write_lock(); - - // record the current authorization server - authorization_state.authorization_server_uri = nmos::experimental::fields::issuer(metadata); + // record the current connected authorization server uri + with_write_lock(authorization_state.mutex, [&] + { + authorization_state.authorization_server_uri = nmos::experimental::fields::issuer(metadata); + }); // cache the authorization server metadata nmos::experimental::update_authorization_server_metadata(authorization_state, metadata); @@ -1304,14 +1293,8 @@ namespace nmos pplx::task request = pplx::task_from_result(); bool registered(false); - - web::json::value client_metadata{}; - web::json::value authorization_server_metadata{}; - with_read_lock(authorization_state.mutex, [&] - { - client_metadata = nmos::experimental::get_client_metadata(authorization_state); - authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); - }); + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); // is client already registered to the Authorization server if(client_metadata.is_null()) @@ -1378,12 +1361,9 @@ namespace nmos client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); } - // store client_credentials to cache + // store client metadata to settings // hmm, may store the only required fields - with_write_lock(authorization_state.mutex, [&] - { - nmos::experimental::update_client_metadata(authorization_state, client_metadata); - }); + nmos::experimental::update_client_metadata(authorization_state, client_metadata); // do callback to safely store the client metadata // Client metadata SHOULD be stored by the client in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. @@ -1492,7 +1472,7 @@ namespace nmos } const auto token_endpoint_auth_method = token_endpoint_auth_method_from_settings(model.settings); - const auto& authorization_server_metadata = with_read_lock(authorization_state.mutex, [&] { return get_authorization_server_metadata(authorization_state); }); + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); const auto& registration_endpoint = web::uri(nmos::experimental::fields::registration_endpoint(authorization_server_metadata)); const auto& issuer = nmos::experimental::fields::issuer(authorization_server_metadata); const auto jwks_uri = make_jwks_uri(model.settings); @@ -1548,10 +1528,7 @@ namespace nmos // store client metadata to settings // hmm, may store the only required fields - with_write_lock(authorization_state.mutex, [&] - { - nmos::experimental::update_client_metadata(authorization_state, client_metadata); - }); + nmos::experimental::update_client_metadata(authorization_state, client_metadata); // hmm, do a callback allowing user to store the client credentials // Client credentials SHOULD be stored by the client in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. @@ -1630,11 +1607,10 @@ namespace nmos const auto& settings = model.settings; - const auto authorization_server_metadata = with_read_lock(authorization_state.mutex, [&] { return get_authorization_server_metadata(authorization_state); }); + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); const web::uri authorization_endpoint(nmos::experimental::fields::authorization_endpoint(authorization_server_metadata)); const auto code_challenge_methods_supported(nmos::experimental::fields::code_challenge_methods_supported(authorization_server_metadata)); - - const auto client_metadata = with_read_lock(authorization_state.mutex, [&] { return get_client_metadata(authorization_state); }); + const auto client_metadata = get_client_metadata(authorization_state); const auto client_id(nmos::experimental::fields::client_id(client_metadata)); const web::uri redirct_uri(nmos::experimental::fields::redirect_uris(client_metadata).size() ? nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string() : U("")); const auto scopes = nmos::experimental::details::scopes(nmos::experimental::fields::scope(client_metadata)); @@ -1726,7 +1702,7 @@ namespace nmos auto& condition = model.condition; auto& shutdown = model.shutdown; - const auto authorization_server_metadata = with_read_lock(authorization_state.mutex, [&] { return get_authorization_server_metadata(authorization_state); }); + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); const web::uri jwks_uri(nmos::experimental::fields::jwks_uri(authorization_server_metadata)); const web::uri token_endpoint(nmos::experimental::fields::token_endpoint(authorization_server_metadata)); const auto& authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); @@ -1737,7 +1713,7 @@ namespace nmos pplx::cancellation_token_source cancellation_source; - auto pubkeys_request(pplx::task_from_result()); + auto pubkeys_requests(pplx::task_from_result()); with_write_lock(authorization_state.mutex, [&] { @@ -1747,13 +1723,13 @@ namespace nmos pubkeys_state.issuer = nmos::experimental::fields::issuer(authorization_server_metadata); }); - auto bearer_token_request(pplx::task_from_result()); + auto bearer_token_requests(pplx::task_from_result()); web::http::oauth2::experimental::oauth2_token bearer_token; std::set scopes; + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); nmos::with_read_lock(authorization_state.mutex, [&] { bearer_token = authorization_state.bearer_token.is_valid_access_token() ? authorization_state.bearer_token : web::http::oauth2::experimental::oauth2_token{}; - const auto& client_metadata = nmos::experimental::get_client_metadata(authorization_state); scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); }); token_shared_state token_state( @@ -1770,13 +1746,13 @@ namespace nmos // start a background task to fetch public keys from authorization server if (nmos::experimental::fields::server_authorization(model.settings)) { - pubkeys_request = request_public_keys(model, authorization_state, authorization_state.pubkeys_state, authorization_service_error, gate, token); + pubkeys_requests = do_public_keys_requests(model, authorization_state, authorization_state.pubkeys_state, authorization_service_error, gate, token); } // start a background task to fetch bearer access token from authorization server if (nmos::experimental::fields::client_authorization(model.settings) && scopes.size()) { - bearer_token_request = request_token(model, authorization_state, token_state, authorization_service_error, gate, token); + bearer_token_requests = do_token_requests(model, authorization_state, token_state, authorization_service_error, gate, token); } // wait for the request because interactions with the Authorization API endpoint must be sequential @@ -1785,8 +1761,8 @@ namespace nmos cancellation_source.cancel(); // wait without the lock since it is also used by the background tasks nmos::details::reverse_lock_guard unlock{ lock }; - pubkeys_request.wait(); - bearer_token_request.wait(); + pubkeys_requests.wait(); + bearer_token_requests.wait(); } // make an asynchronously GET request over the Token Issuer to fetch issuer metadata @@ -1798,7 +1774,6 @@ namespace nmos bool authorization_service_error(false); - std::unique_ptr client; bool metadata_received(false); pplx::cancellation_token_source cancellation_source; @@ -1810,32 +1785,30 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer metadata fetch"; - web::uri token_issuer; - std::set scopes; - std::set grants; - web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method; - nmos::with_write_lock(authorization_state.mutex, [&] + const auto token_issuer = nmos::with_write_lock(authorization_state.mutex, [&] { authorization_state.fetch_token_issuer_pubkeys = false; - token_issuer = authorization_state.token_issuer; - - const auto& client_metadata = nmos::experimental::get_client_metadata(authorization_state); - scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); - grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); - token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); + return authorization_state.token_issuer; }); + if (token_issuer.is_empty()) + { + slog::log(gate, SLOG_FLF) << "No authorization token's issuer to fetch server metadata"; + return false; + } - if (token_issuer.is_empty()) return false; + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + const auto grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); + const auto token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); slog::log(gate, SLOG_FLF) << "Using authorization token's issuer " << utility::us2s(token_issuer.to_string()) << " to fetch server metadata"; - client.reset(new web::http::client::http_client(make_authorization_service_uri(token_issuer), make_authorization_http_client_config(model.settings, load_ca_certificates, gate))); + + web::http::client::http_client client(make_authorization_service_uri(token_issuer), make_authorization_http_client_config(model.settings, load_ca_certificates, gate)); auto token = cancellation_source.get_token(); - auto request = details::request_authorization_server_metadata(*client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&](web::json::value metadata) + auto request = details::request_authorization_server_metadata(client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&](web::json::value metadata) { - auto lock = authorization_state.write_lock(); - // cache the issuer metadata nmos::experimental::update_authorization_server_metadata(authorization_state, token_issuer, metadata); @@ -1909,13 +1882,9 @@ namespace nmos pplx::cancellation_token_source cancellation_source; - web::uri token_issuer; - web::uri jwks_uri; - with_read_lock(authorization_state.mutex, [&] - { - token_issuer = authorization_state.token_issuer; - jwks_uri = nmos::experimental::fields::jwks_uri(get_authorization_server_metadata(authorization_state, token_issuer)); - }); + const auto token_issuer = with_read_lock(authorization_state.mutex, [&] { return authorization_state.token_issuer; }); + const auto jwks_uri = nmos::experimental::fields::jwks_uri(get_authorization_server_metadata(authorization_state, token_issuer)); + auto authorization_version = version(token_issuer); pubkeys_shared_state pubkeys_state( { jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, @@ -1936,7 +1905,7 @@ namespace nmos auto token = cancellation_source.get_token(); // start a one-shot background task to fetch public keys from the token issuer - auto pubkeys_request = request_public_keys(model, authorization_state, pubkeys_state, authorization_service_error, gate, token); + auto pubkeys_requests = do_public_keys_requests(model, authorization_state, pubkeys_state, authorization_service_error, gate, token); // wait for the request because interactions with the Authorization API endpoint must be sequential condition.wait(lock, [&] { return shutdown || authorization_service_error || pubkeys_state.received; }); @@ -1944,7 +1913,7 @@ namespace nmos cancellation_source.cancel(); // wait without the lock since it is also used by the background tasks nmos::details::reverse_lock_guard unlock{ lock }; - pubkeys_request.wait(); + pubkeys_requests.wait(); } } } diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp index c79d0b40a..09e342e6f 100644 --- a/Development/nmos/authorization_redirect_api.cpp +++ b/Development/nmos/authorization_redirect_api.cpp @@ -156,16 +156,14 @@ namespace nmos utility::string_t state; utility::string_t code_verifier; - web::json::value authorization_server_metadata; - web::json::value client_metadata; with_write_lock(authorization_state.mutex, [&] { state = authorization_state.state; code_verifier = authorization_state.code_verifier; authorization_state.authorization_flow = authorization_state::request_code; - authorization_server_metadata = get_authorization_server_metadata(authorization_state); - client_metadata = get_client_metadata(authorization_state); }); + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); + const auto client_metadata = get_client_metadata(authorization_state); web::uri token_endpoint; web::http::client::http_client_config config; diff --git a/Development/nmos/authorization_state.cpp b/Development/nmos/authorization_state.cpp index f4c03b135..297c06a30 100644 --- a/Development/nmos/authorization_state.cpp +++ b/Development/nmos/authorization_state.cpp @@ -8,6 +8,8 @@ namespace nmos { web::json::value get_authorization_server_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri) { + auto lock = authorization_state.read_lock(); + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { @@ -22,6 +24,8 @@ namespace nmos web::json::value get_client_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri) { + auto lock = authorization_state.read_lock(); + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { @@ -36,6 +40,8 @@ namespace nmos web::json::value get_jwks(const authorization_state& authorization_state, const web::uri& authorization_server_uri) { + auto lock = authorization_state.read_lock(); + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { @@ -50,16 +56,18 @@ namespace nmos void update_authorization_server_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& authorization_server_metadata) { + auto lock = authorization_state.write_lock(); + auto issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { - // update + // update the relevant issuer's metadata auto& settings = issuer->second.settings; settings[nmos::experimental::fields::authorization_server_metadata] = authorization_server_metadata; } else { - // insert + // insert a new issuer with metadata authorization_state.issuers.insert(std::make_pair( authorization_server_uri.to_string(), { web::json::value_of({ @@ -77,16 +85,18 @@ namespace nmos void update_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& client_metadata) { + auto lock = authorization_state.write_lock(); + auto issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { - // update + // update the relevant issuer's client_metadata auto& settings = issuer->second.settings; settings[nmos::experimental::fields::client_metadata] = client_metadata; } else { - // insert + // insert a new issuer with client_metadata authorization_state.issuers.insert(std::make_pair( authorization_server_uri.to_string(), { web::json::value_of({ @@ -104,17 +114,20 @@ namespace nmos void update_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator) { + auto lock = authorization_state.write_lock(); + auto issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { - // update + // update the relevant issuer's jwks auto& settings = issuer->second.settings; settings[nmos::experimental::fields::jwks] = jwks; + // update relevant issuer's jwt_validator, which was constructed by the jwks issuer->second.jwt_validator = jwt_validator; } else { - // insert + // insert a new issuer with issuer's jwks and issuer's jwt_validator authorization_state.issuers.insert(std::make_pair( authorization_server_uri.to_string(), { web::json::value_of({ @@ -131,6 +144,8 @@ namespace nmos void erase_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri) { + auto lock = authorization_state.write_lock(); + auto issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { @@ -146,6 +161,8 @@ namespace nmos void erase_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri) { + auto lock = authorization_state.write_lock(); + auto issuer = authorization_state.issuers.find(authorization_server_uri); if (authorization_state.issuers.end() != issuer) { From 58e89263d7ca5f3da9f684c3c74f8f158810915a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 2 Oct 2023 12:17:08 +0100 Subject: [PATCH 068/121] Remove unnecessary debug traces --- .github/workflows/src/amwa-test.yml | 2 -- .github/workflows/src/build-setup.yml | 2 -- Sandbox/run_nmos_testing.sh | 26 -------------------------- 3 files changed, 30 deletions(-) diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 0d515900e..d9ae89293 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -71,8 +71,6 @@ if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py - - cat nmostesting/suites/BCP00301Test.py fi # install certificates diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index a2941c428..2559b3092 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -41,8 +41,6 @@ Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" - # display the hosts contents - type $env:WINDIR\System32\Drivers\Etc\Hosts # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index c54ffb090..87dd27988 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -146,32 +146,6 @@ fi "${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params} ${node_params}}" > ${results_dir}/nodeoutput 2>&1 & NODE_PID=$! - -if [[ "${config_dns_sd_mode}" == "multicast" ]]; then - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "1 $IP4" - sleep 1 - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "2 $IP4" - sleep 2 - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "3 $IP4" - sleep 4 - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "4 $IP4" - sleep 8 - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "5 $IP4" - sleep 16 - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "6 $IP4" - sleep 32 - IP4=$(dig -r @224.0.0.251 -p 5353 +short -t a +notcp nmos-api.local 2>/dev/null | sed '/^;;/d') - echo "7 $IP4" -fi - - - function do_run_test() { suite=$1 echo "Running $suite" From de8facbc83911e1afd17c7697bd715f88480e36b Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 2 Oct 2023 12:18:10 +0100 Subject: [PATCH 069/121] Start auth test on windows plaforms --- .github/workflows/build-test.yml | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 83178f79c..33873358f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -133,8 +133,6 @@ jobs: Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" - # display the hosts contents - type $env:WINDIR\System32\Drivers\Etc\Hosts # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg @@ -347,17 +345,16 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - # printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token - # if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # hostname=nmos-api.local - # else - # hostname=api.testsuite.nmos.tv - # fi - # printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + hostname=nmos-api.local + else + hostname=api.testsuite.nmos.tv + fi + printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py # Download testssl cd testssl @@ -396,11 +393,10 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - # if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - # printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py - # cat nmostesting/suites/BCP00301Test.py - # fi + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test + printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + fi # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem @@ -671,8 +667,6 @@ jobs: Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" - # display the hosts contents - type $env:WINDIR\System32\Drivers\Etc\Hosts # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg @@ -936,8 +930,6 @@ jobs: if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py - - cat nmostesting/suites/BCP00301Test.py fi # install certificates From e0433767bf19234352c30c85b031ada435ebc52c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 3 Oct 2023 12:42:30 +0100 Subject: [PATCH 070/121] Due to unable to perform hostname to ip lookups in testssl.sh, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests --- .github/workflows/build-test.yml | 8 ++++---- .github/workflows/src/amwa-test.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 33873358f..333afe093 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -394,8 +394,8 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests + printf '\n\ndef perform_test_ssl_with_ip(self, test, args=None):\n if os.path.exists(TMPFILE):\n os.remove(TMPFILE)\n if args is None:\n args = []\n arg_key = " ".join(args)\n if arg_key in self.report_json:\n return self.report_json[arg_key]\n else:\n try:\n print(args)\n print(arg_key)\n ret = subprocess.run(\n [\n CONFIG.TEST_SSL_BASH,\n "testssl/testssl.sh",\n "--jsonfile",\n TMPFILE,\n "--warnings",\n "off",\n "--openssl-timeout",\n str(CONFIG.HTTP_TIMEOUT),\n "--add-ca",\n CONFIG.CERT_TRUST_ROOT_CA,\n "--ip",\n socket.gethostbyname(self.apis[SECURE_API_KEY]["hostname"]),\n ]\n + args\n + [\n "{}:{}".format(\n self.apis[SECURE_API_KEY]["hostname"],\n self.apis[SECURE_API_KEY]["port"],\n )\n ]\n )\n if ret.returncode == 0:\n with open(TMPFILE) as tls_data:\n self.report_json[arg_key] = json.load(tls_data)\n return self.report_json[arg_key]\n except Exception as e:\n raise NMOSTestException(\n test.DISABLED(\n "Unable to execute testssl.sh. Please see the README for "\n "installation instructions: {}".format(e)\n )\n )\n return None\n\n\nBCP00301Test.perform_test_ssl = perform_test_ssl_with_ip\n' >> nmostesting/suites/BCP00301Test.py fi # install certificates @@ -928,8 +928,8 @@ jobs: if [[ "${{ runner.os }}" == "Windows" ]]; then if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests + printf '\n\ndef perform_test_ssl_with_ip(self, test, args=None):\n if os.path.exists(TMPFILE):\n os.remove(TMPFILE)\n if args is None:\n args = []\n arg_key = " ".join(args)\n if arg_key in self.report_json:\n return self.report_json[arg_key]\n else:\n try:\n print(args)\n print(arg_key)\n ret = subprocess.run(\n [\n CONFIG.TEST_SSL_BASH,\n "testssl/testssl.sh",\n "--jsonfile",\n TMPFILE,\n "--warnings",\n "off",\n "--openssl-timeout",\n str(CONFIG.HTTP_TIMEOUT),\n "--add-ca",\n CONFIG.CERT_TRUST_ROOT_CA,\n "--ip",\n socket.gethostbyname(self.apis[SECURE_API_KEY]["hostname"]),\n ]\n + args\n + [\n "{}:{}".format(\n self.apis[SECURE_API_KEY]["hostname"],\n self.apis[SECURE_API_KEY]["port"],\n )\n ]\n )\n if ret.returncode == 0:\n with open(TMPFILE) as tls_data:\n self.report_json[arg_key] = json.load(tls_data)\n return self.report_json[arg_key]\n except Exception as e:\n raise NMOSTestException(\n test.DISABLED(\n "Unable to execute testssl.sh. Please see the README for "\n "installation instructions: {}".format(e)\n )\n )\n return None\n\n\nBCP00301Test.perform_test_ssl = perform_test_ssl_with_ip\n' >> nmostesting/suites/BCP00301Test.py fi # install certificates diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index d9ae89293..7e91ff390 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -69,8 +69,8 @@ if [[ "${{ runner.os }}" == "Windows" ]]; then if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 test - printf '\n\ndef new_BCP00301test_01(self, test):\n """TLS Protocols"""\n hostname = self.apis[SECURE_API_KEY]["hostname"]\n tls_data = self.perform_test_ssl(test, ["-p", "--ip={}".format(socket.gethostbyname(hostname))])\n if tls_data is None:\n return test.DISABLED("Unable to test. See the console for further information.")\n else:\n for report in tls_data:\n if report["id"] in ["SSLv2", "SSLv3", "TLS1", "TLS1_1"] and "not offered" not in report["finding"]:\n return test.FAIL("Protocol {} must not be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_2"] and not report["finding"].startswith("offered"):\n return test.FAIL("Protocol {} must be offered".format(report["id"].replace("_", ".")))\n elif report["id"] in ["TLS1_3"] and not report["finding"].startswith("offered"):\n return test.OPTIONAL("Protocol {} should be offered".format(report["id"].replace("_", ".")),\n "https://specs.amwa.tv/bcp-003-01/branches/{}"\n "/docs/Secure_Communication.html#tls-versions"\n .format(self.apis[SECURE_API_KEY]["spec_branch"]))\n return test.PASS()\n\n\nBCP00301Test.test_01 = new_BCP00301test_01\n' >> nmostesting/suites/BCP00301Test.py + # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests + printf '\n\ndef perform_test_ssl_with_ip(self, test, args=None):\n if os.path.exists(TMPFILE):\n os.remove(TMPFILE)\n if args is None:\n args = []\n arg_key = " ".join(args)\n if arg_key in self.report_json:\n return self.report_json[arg_key]\n else:\n try:\n print(args)\n print(arg_key)\n ret = subprocess.run(\n [\n CONFIG.TEST_SSL_BASH,\n "testssl/testssl.sh",\n "--jsonfile",\n TMPFILE,\n "--warnings",\n "off",\n "--openssl-timeout",\n str(CONFIG.HTTP_TIMEOUT),\n "--add-ca",\n CONFIG.CERT_TRUST_ROOT_CA,\n "--ip",\n socket.gethostbyname(self.apis[SECURE_API_KEY]["hostname"]),\n ]\n + args\n + [\n "{}:{}".format(\n self.apis[SECURE_API_KEY]["hostname"],\n self.apis[SECURE_API_KEY]["port"],\n )\n ]\n )\n if ret.returncode == 0:\n with open(TMPFILE) as tls_data:\n self.report_json[arg_key] = json.load(tls_data)\n return self.report_json[arg_key]\n except Exception as e:\n raise NMOSTestException(\n test.DISABLED(\n "Unable to execute testssl.sh. Please see the README for "\n "installation instructions: {}".format(e)\n )\n )\n return None\n\n\nBCP00301Test.perform_test_ssl = perform_test_ssl_with_ip\n' >> nmostesting/suites/BCP00301Test.py fi # install certificates From 11717da12a95e5f2936bcc7cf8a254b241f1e04d Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 3 Oct 2023 15:30:25 +0100 Subject: [PATCH 071/121] Up the service_unavailable_retry_after to 25s as MacOS sometime taken longer than 15 seconds to obtain the public keys from the token issuer (including fetchng the isser metadata then the publick keys) --- Sandbox/run_nmos_testing.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 87dd27988..d01d4e6d4 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -112,7 +112,7 @@ if [[ "${config_auth}" == "True" ]]; then auth=true common_params+=",\ \"server_authorization\":true,\ - \"service_unavailable_retry_after\":15\ + \"service_unavailable_retry_after\":25\ " registry_params=",\ \"label\":\"nmos-cpp-registry\"\ From 3a48438a0f65e7ea93594916b7d5d9fc329e848a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 4 Oct 2023 17:59:16 +0100 Subject: [PATCH 072/121] In AUTH mode macOS Registry could take up to 20+ seconds for obtaining the Authorization Server Public Keys. --- .github/workflows/build-test.yml | 4 ++++ .github/workflows/src/amwa-test.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 333afe093..92ec565bf 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -420,6 +420,8 @@ jobs: mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py + # in AUTH mode macos could take up to 20+ seconds for obtaining the Authorization Server Public Keys + printf '\n\ndef run_noninteractive_tests_with_delay(args):\n endpoints = []\n for i in range(len(args.host)):\n if args.host[i] == "null":\n args.host[i] = None\n if args.port[i] == 0:\n args.port[i] = None\n if args.version[i] == "null":\n args.version[i] = None\n selector = None\n if len(args.selector) == len(args.host) and args.selector[i] != "null":\n selector = args.selector[i]\n endpoints.append(\n {\n "host": args.host[i],\n "port": args.port[i],\n "version": args.version[i],\n "selector": selector,\n }\n )\n try:\n # in AUTH mode macOS could take up to 20+ seconds for obtaining the Authorization Server Public Keys\n if CONFIG.ENABLE_AUTH:\n time.sleep(30)\n results = run_tests(args.suite, endpoints, [args.selection])\n if args.output:\n exit_code = write_test_results(results, endpoints, args)\n else:\n exit_code = print_test_results(results, endpoints, args)\n except Exception as e:\n print(" * ERROR: {}".format(str(e)))\n exit_code = ExitCodes.ERROR\n return exit_code\n\n\nrun_noninteractive_tests = run_noninteractive_tests_with_delay\n' >> nmostesting/NMOSTesting.py fi if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then @@ -954,6 +956,8 @@ jobs: mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py + # in AUTH mode macos could take up to 20+ seconds for obtaining the Authorization Server Public Keys + printf '\n\ndef run_noninteractive_tests_with_delay(args):\n endpoints = []\n for i in range(len(args.host)):\n if args.host[i] == "null":\n args.host[i] = None\n if args.port[i] == 0:\n args.port[i] = None\n if args.version[i] == "null":\n args.version[i] = None\n selector = None\n if len(args.selector) == len(args.host) and args.selector[i] != "null":\n selector = args.selector[i]\n endpoints.append(\n {\n "host": args.host[i],\n "port": args.port[i],\n "version": args.version[i],\n "selector": selector,\n }\n )\n try:\n # in AUTH mode macOS could take up to 20+ seconds for obtaining the Authorization Server Public Keys\n if CONFIG.ENABLE_AUTH:\n time.sleep(30)\n results = run_tests(args.suite, endpoints, [args.selection])\n if args.output:\n exit_code = write_test_results(results, endpoints, args)\n else:\n exit_code = print_test_results(results, endpoints, args)\n except Exception as e:\n print(" * ERROR: {}".format(str(e)))\n exit_code = ExitCodes.ERROR\n return exit_code\n\n\nrun_noninteractive_tests = run_noninteractive_tests_with_delay\n' >> nmostesting/NMOSTesting.py fi if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 7e91ff390..165f446fa 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -95,6 +95,8 @@ mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py + # in AUTH mode macos could take up to 20+ seconds for obtaining the Authorization Server Public Keys + printf '\n\ndef run_noninteractive_tests_with_delay(args):\n endpoints = []\n for i in range(len(args.host)):\n if args.host[i] == "null":\n args.host[i] = None\n if args.port[i] == 0:\n args.port[i] = None\n if args.version[i] == "null":\n args.version[i] = None\n selector = None\n if len(args.selector) == len(args.host) and args.selector[i] != "null":\n selector = args.selector[i]\n endpoints.append(\n {\n "host": args.host[i],\n "port": args.port[i],\n "version": args.version[i],\n "selector": selector,\n }\n )\n try:\n # in AUTH mode macOS could take up to 20+ seconds for obtaining the Authorization Server Public Keys\n if CONFIG.ENABLE_AUTH:\n time.sleep(30)\n results = run_tests(args.suite, endpoints, [args.selection])\n if args.output:\n exit_code = write_test_results(results, endpoints, args)\n else:\n exit_code = print_test_results(results, endpoints, args)\n except Exception as e:\n print(" * ERROR: {}".format(str(e)))\n exit_code = ExitCodes.ERROR\n return exit_code\n\n\nrun_noninteractive_tests = run_noninteractive_tests_with_delay\n' >> nmostesting/NMOSTesting.py fi if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then From 67d8de3950479c986e443ba7d28ac25786f5e74c Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 4 Oct 2023 23:45:26 +0100 Subject: [PATCH 073/121] Fix fetch token issuer's public keys, in event of receiving unknown issuer token --- Development/nmos/authorization_operation.cpp | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 39b0df5e0..bcb626363 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -303,7 +303,7 @@ namespace nmos // hmm, verify Authorization server meeting the minimum client requirement - // is the required response_types supported by Authorization server (response_types_supported) + // is the required response_types supported by the Authorization server std::set response_types = { response_types::code }; if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return grant_types::implicit == grant; })) { @@ -325,9 +325,9 @@ namespace nmos } // scopes_supported is optional - // is required scopes supported by Authorization server (scopes_supported) if (scopes.size() && metadata.has_array_field(nmos::experimental::fields::scopes_supported)) { + // is the required scopes supported by the Authorization server const auto supported = std::all_of(scopes.begin(), scopes.end(), [&](const nmos::experimental::scope& scope) { const auto& scopes_supported = nmos::experimental::fields::scopes_supported(metadata); @@ -342,9 +342,9 @@ namespace nmos } // grant_types_supported is optional - // is required grants supported by Authorization server (grant_types_supported) if (grants.size() && metadata.has_array_field(nmos::experimental::fields::grant_types_supported)) { + // is the required grants supported by the Authorization server const auto supported = std::all_of(grants.begin(), grants.end(), [&](const web::http::oauth2::experimental::grant_type& grant) { const auto& grants_supported = nmos::experimental::fields::grant_types_supported(metadata); @@ -359,9 +359,9 @@ namespace nmos } // token_endpoint_auth_methods_supported is optional - // is required token_endpoint_auth_method supported by Authorization server (token_endpoint_auth_methods_supported) if (metadata.has_array_field(nmos::experimental::fields::token_endpoint_auth_methods_supported)) { + // is the required token_endpoint_auth_method supported by the Authorization server const auto& supported = nmos::experimental::fields::token_endpoint_auth_methods_supported(metadata); const auto found = std::find_if(supported.begin(), supported.end(), [&token_endpoint_auth_method](const web::json::value& token_endpoint_auth_method_) { return token_endpoint_auth_method_.as_string() == token_endpoint_auth_method.name; }); if (supported.end() == found) @@ -1038,11 +1038,10 @@ namespace nmos fetch_interval = std::chrono::seconds((int)(std::uniform_real_distribution<>(fetch_interval_min, fetch_interval_max)(pubkeys_state.engine))); } nmos::with_write_lock(authorization_state.mutex, [&] { pubkeys_state.immediate = false; }); - slog::log(gate, SLOG_FLF) << "Requesting authorization public keys (jwks) for about " << fetch_interval.count() << " seconds"; auto fetch_time = std::chrono::steady_clock::now(); - return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() //mutable + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() { auto lock = authorization_state.read_lock(); @@ -1093,18 +1092,18 @@ namespace nmos authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(auth_version)); // may throw json_exception })); - slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys from " << utility::us2s(issuer.to_string()); + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys for " << utility::us2s(issuer.to_string()); } else { nmos::experimental::erase_jwks(authorization_state, issuer); - slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list from " << utility::us2s(issuer.to_string()); + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list for " << utility::us2s(issuer.to_string()); } } else { - slog::log(gate, SLOG_FLF) << "No public keys changes found from " << utility::us2s(issuer.to_string()); + slog::log(gate, SLOG_FLF) << "No public keys changes found for " << utility::us2s(issuer.to_string()); } return !one_shot; @@ -1807,7 +1806,7 @@ namespace nmos auto token = cancellation_source.get_token(); - auto request = details::request_authorization_server_metadata(client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&](web::json::value metadata) + auto request = details::request_authorization_server_metadata(client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&, token_issuer](web::json::value metadata) { // cache the issuer metadata nmos::experimental::update_authorization_server_metadata(authorization_state, token_issuer, metadata); @@ -1872,7 +1871,7 @@ namespace nmos // make an asynchronously GET request over the Token Issuer to fetch public keys void request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer's public keys"; + slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer's public keys fetch"; auto lock = model.write_lock(); auto& condition = model.condition; From c4e30d3c7703aa1c03081aea58422c682f6d616a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 5 Oct 2023 14:29:07 +0100 Subject: [PATCH 074/121] Fix fetch token issuer's public keys, in event of receiving unknown issuer token --- Development/nmos/api_utils.cpp | 10 ++++++---- Development/nmos/events_ws_api.cpp | 5 ++++- Development/nmos/ws_api_utils.cpp | 6 +++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 62171e014..6e2f0fbc3 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -774,11 +774,12 @@ namespace nmos if (methods::OPTIONS == req.method()) return pplx::task_from_result(true); + web::uri token_issuer; const auto audience = with_read_lock(model.mutex, [&] { return nmos::get_host_name(model.settings); }); - auto error = with_write_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &gate_] + auto error = with_read_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &token_issuer, &gate_] { - // note: the validate_authorization will update the authorization_state.token_issuer, i.e. using with_write_lock to protected it - return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, authorization_state.token_issuer, gate_); + // note: the validate_authorization will update the token_issuer, i.e. using with_write_lock to protected it + return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, token_issuer, gate_); }); if (error) @@ -794,9 +795,10 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Authorization warning: " << error.message; - with_write_lock(authorization_state.mutex, [&authorization_state] + with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] { authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; }); auto lock = model.write_lock(); diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index 4facef72f..ab0b54a43 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -78,8 +78,10 @@ namespace nmos { const auto& settings = model.settings; + web::uri token_issuer; + authorization_state.write_lock(); - const auto error = nmos::experimental::ws_validate_authorization(authorization_state.issuers, req, nmos::experimental::scopes::events, nmos::get_host_name(settings), authorization_state.token_issuer, gate_); + const auto error = nmos::experimental::ws_validate_authorization(authorization_state.issuers, req, nmos::experimental::scopes::events, nmos::get_host_name(settings), token_issuer, gate_); if (error) { // set error repsonse @@ -95,6 +97,7 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << error.message; authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; model.notify(); } else diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp index 5f762de0d..1c0e446a9 100644 --- a/Development/nmos/ws_api_utils.cpp +++ b/Development/nmos/ws_api_utils.cpp @@ -20,8 +20,11 @@ namespace nmos { const auto& settings = model.settings; + web::uri token_issuer; + authorization_state.write_lock(); - const auto error = ws_validate_authorization(authorization_state.issuers, request, scope, nmos::get_host_name(settings), authorization_state.token_issuer, gate); + + const auto error = ws_validate_authorization(authorization_state.issuers, request, scope, nmos::get_host_name(settings), token_issuer, gate); if (error) { // set error repsonse @@ -37,6 +40,7 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << error.message; authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; model.notify(); } else From ddcc9e62cec7cb2ca1f5b3fc7f657f40d7b7c9f1 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 5 Oct 2023 16:53:56 +0100 Subject: [PATCH 075/121] Rollback to not overriding perform_test_ssl in nmostesting.py to use --ip option for testssl.sh, it is now implemenated in the latest testsuite --- .github/workflows/build-test.yml | 10 ---------- .github/workflows/src/amwa-test.yml | 5 ----- 2 files changed, 15 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 92ec565bf..9e63f77c7 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -393,11 +393,6 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests - printf '\n\ndef perform_test_ssl_with_ip(self, test, args=None):\n if os.path.exists(TMPFILE):\n os.remove(TMPFILE)\n if args is None:\n args = []\n arg_key = " ".join(args)\n if arg_key in self.report_json:\n return self.report_json[arg_key]\n else:\n try:\n print(args)\n print(arg_key)\n ret = subprocess.run(\n [\n CONFIG.TEST_SSL_BASH,\n "testssl/testssl.sh",\n "--jsonfile",\n TMPFILE,\n "--warnings",\n "off",\n "--openssl-timeout",\n str(CONFIG.HTTP_TIMEOUT),\n "--add-ca",\n CONFIG.CERT_TRUST_ROOT_CA,\n "--ip",\n socket.gethostbyname(self.apis[SECURE_API_KEY]["hostname"]),\n ]\n + args\n + [\n "{}:{}".format(\n self.apis[SECURE_API_KEY]["hostname"],\n self.apis[SECURE_API_KEY]["port"],\n )\n ]\n )\n if ret.returncode == 0:\n with open(TMPFILE) as tls_data:\n self.report_json[arg_key] = json.load(tls_data)\n return self.report_json[arg_key]\n except Exception as e:\n raise NMOSTestException(\n test.DISABLED(\n "Unable to execute testssl.sh. Please see the README for "\n "installation instructions: {}".format(e)\n )\n )\n return None\n\n\nBCP00301Test.perform_test_ssl = perform_test_ssl_with_ip\n' >> nmostesting/suites/BCP00301Test.py - fi - # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem @@ -929,11 +924,6 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests - printf '\n\ndef perform_test_ssl_with_ip(self, test, args=None):\n if os.path.exists(TMPFILE):\n os.remove(TMPFILE)\n if args is None:\n args = []\n arg_key = " ".join(args)\n if arg_key in self.report_json:\n return self.report_json[arg_key]\n else:\n try:\n print(args)\n print(arg_key)\n ret = subprocess.run(\n [\n CONFIG.TEST_SSL_BASH,\n "testssl/testssl.sh",\n "--jsonfile",\n TMPFILE,\n "--warnings",\n "off",\n "--openssl-timeout",\n str(CONFIG.HTTP_TIMEOUT),\n "--add-ca",\n CONFIG.CERT_TRUST_ROOT_CA,\n "--ip",\n socket.gethostbyname(self.apis[SECURE_API_KEY]["hostname"]),\n ]\n + args\n + [\n "{}:{}".format(\n self.apis[SECURE_API_KEY]["hostname"],\n self.apis[SECURE_API_KEY]["port"],\n )\n ]\n )\n if ret.returncode == 0:\n with open(TMPFILE) as tls_data:\n self.report_json[arg_key] = json.load(tls_data)\n return self.report_json[arg_key]\n except Exception as e:\n raise NMOSTestException(\n test.DISABLED(\n "Unable to execute testssl.sh. Please see the README for "\n "installation instructions: {}".format(e)\n )\n )\n return None\n\n\nBCP00301Test.perform_test_ssl = perform_test_ssl_with_ip\n' >> nmostesting/suites/BCP00301Test.py - fi - # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 165f446fa..ba6c7d003 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -68,11 +68,6 @@ pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; then - if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then - # due to unable to perform hostname to ip lookups, "--ip" option is used to overcome this issue while doing the BCP-003-01 tests - printf '\n\ndef perform_test_ssl_with_ip(self, test, args=None):\n if os.path.exists(TMPFILE):\n os.remove(TMPFILE)\n if args is None:\n args = []\n arg_key = " ".join(args)\n if arg_key in self.report_json:\n return self.report_json[arg_key]\n else:\n try:\n print(args)\n print(arg_key)\n ret = subprocess.run(\n [\n CONFIG.TEST_SSL_BASH,\n "testssl/testssl.sh",\n "--jsonfile",\n TMPFILE,\n "--warnings",\n "off",\n "--openssl-timeout",\n str(CONFIG.HTTP_TIMEOUT),\n "--add-ca",\n CONFIG.CERT_TRUST_ROOT_CA,\n "--ip",\n socket.gethostbyname(self.apis[SECURE_API_KEY]["hostname"]),\n ]\n + args\n + [\n "{}:{}".format(\n self.apis[SECURE_API_KEY]["hostname"],\n self.apis[SECURE_API_KEY]["port"],\n )\n ]\n )\n if ret.returncode == 0:\n with open(TMPFILE) as tls_data:\n self.report_json[arg_key] = json.load(tls_data)\n return self.report_json[arg_key]\n except Exception as e:\n raise NMOSTestException(\n test.DISABLED(\n "Unable to execute testssl.sh. Please see the README for "\n "installation instructions: {}".format(e)\n )\n )\n return None\n\n\nBCP00301Test.perform_test_ssl = perform_test_ssl_with_ip\n' >> nmostesting/suites/BCP00301Test.py - fi - # install certificates certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem From 68778722d8b72a44aa79cfde0562c16fefeebba6 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 5 Oct 2023 17:13:44 +0100 Subject: [PATCH 076/121] Rollback to not overriding run-noninteractive_tests in NMOSTesting.py to add delay before starting tests, it is now implemenated in the latest testsuite --- .github/workflows/build-test.yml | 8 ++------ .github/workflows/src/amwa-test.yml | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9e63f77c7..e0118a265 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -345,7 +345,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -415,8 +415,6 @@ jobs: mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py - # in AUTH mode macos could take up to 20+ seconds for obtaining the Authorization Server Public Keys - printf '\n\ndef run_noninteractive_tests_with_delay(args):\n endpoints = []\n for i in range(len(args.host)):\n if args.host[i] == "null":\n args.host[i] = None\n if args.port[i] == 0:\n args.port[i] = None\n if args.version[i] == "null":\n args.version[i] = None\n selector = None\n if len(args.selector) == len(args.host) and args.selector[i] != "null":\n selector = args.selector[i]\n endpoints.append(\n {\n "host": args.host[i],\n "port": args.port[i],\n "version": args.version[i],\n "selector": selector,\n }\n )\n try:\n # in AUTH mode macOS could take up to 20+ seconds for obtaining the Authorization Server Public Keys\n if CONFIG.ENABLE_AUTH:\n time.sleep(30)\n results = run_tests(args.suite, endpoints, [args.selection])\n if args.output:\n exit_code = write_test_results(results, endpoints, args)\n else:\n exit_code = print_test_results(results, endpoints, args)\n except Exception as e:\n print(" * ERROR: {}".format(str(e)))\n exit_code = ExitCodes.ERROR\n return exit_code\n\n\nrun_noninteractive_tests = run_noninteractive_tests_with_delay\n' >> nmostesting/NMOSTesting.py fi if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then @@ -876,7 +874,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -946,8 +944,6 @@ jobs: mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py - # in AUTH mode macos could take up to 20+ seconds for obtaining the Authorization Server Public Keys - printf '\n\ndef run_noninteractive_tests_with_delay(args):\n endpoints = []\n for i in range(len(args.host)):\n if args.host[i] == "null":\n args.host[i] = None\n if args.port[i] == 0:\n args.port[i] = None\n if args.version[i] == "null":\n args.version[i] = None\n selector = None\n if len(args.selector) == len(args.host) and args.selector[i] != "null":\n selector = args.selector[i]\n endpoints.append(\n {\n "host": args.host[i],\n "port": args.port[i],\n "version": args.version[i],\n "selector": selector,\n }\n )\n try:\n # in AUTH mode macOS could take up to 20+ seconds for obtaining the Authorization Server Public Keys\n if CONFIG.ENABLE_AUTH:\n time.sleep(30)\n results = run_tests(args.suite, endpoints, [args.selection])\n if args.output:\n exit_code = write_test_results(results, endpoints, args)\n else:\n exit_code = print_test_results(results, endpoints, args)\n except Exception as e:\n print(" * ERROR: {}".format(str(e)))\n exit_code = ExitCodes.ERROR\n return exit_code\n\n\nrun_noninteractive_tests = run_noninteractive_tests_with_delay\n' >> nmostesting/NMOSTesting.py fi if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index ba6c7d003..fb698b364 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -20,7 +20,7 @@ cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -90,8 +90,6 @@ mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py - # in AUTH mode macos could take up to 20+ seconds for obtaining the Authorization Server Public Keys - printf '\n\ndef run_noninteractive_tests_with_delay(args):\n endpoints = []\n for i in range(len(args.host)):\n if args.host[i] == "null":\n args.host[i] = None\n if args.port[i] == 0:\n args.port[i] = None\n if args.version[i] == "null":\n args.version[i] = None\n selector = None\n if len(args.selector) == len(args.host) and args.selector[i] != "null":\n selector = args.selector[i]\n endpoints.append(\n {\n "host": args.host[i],\n "port": args.port[i],\n "version": args.version[i],\n "selector": selector,\n }\n )\n try:\n # in AUTH mode macOS could take up to 20+ seconds for obtaining the Authorization Server Public Keys\n if CONFIG.ENABLE_AUTH:\n time.sleep(30)\n results = run_tests(args.suite, endpoints, [args.selection])\n if args.output:\n exit_code = write_test_results(results, endpoints, args)\n else:\n exit_code = print_test_results(results, endpoints, args)\n except Exception as e:\n print(" * ERROR: {}".format(str(e)))\n exit_code = ExitCodes.ERROR\n return exit_code\n\n\nrun_noninteractive_tests = run_noninteractive_tests_with_delay\n' >> nmostesting/NMOSTesting.py fi if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then From f4fae57baaa83036379cfd04ec9e6af03a6847eb Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 9 Oct 2023 12:21:58 +0100 Subject: [PATCH 077/121] Extend the HTTP_TIMEOUT to identify the case of the mock jwks endpoint not responding --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index e0118a265..9540e66df 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -345,7 +345,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 100\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -874,7 +874,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 100\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index fb698b364..9783b504e 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -20,7 +20,7 @@ cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 100\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token From 039dbaeb9c17c746e015e0fa2cc82ef28ec0354b Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 9 Oct 2023 15:57:35 +0100 Subject: [PATCH 078/121] Stick to their relevant Authoriztaion servers for the authorization_behaviour_thread and the authorization_token_issuer_thread --- Development/nmos/authorization_operation.cpp | 206 +++++++++++++------ Development/nmos/authorization_operation.h | 6 +- Development/nmos/authorization_state.h | 8 +- 3 files changed, 152 insertions(+), 68 deletions(-) diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index bcb626363..139dbc3ad 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -1032,7 +1032,7 @@ namespace nmos if (nmos::with_read_lock(authorization_state.mutex, [&] { auto issuer = authorization_state.issuers.find(pubkeys_state.issuer.to_string()); - return ((authorization_state.issuers.end() != issuer) && !pubkeys_state.one_shot && !pubkeys_state.immediate); + return ((authorization_state.issuers.end() != issuer) && !pubkeys_state.immediate); })) { fetch_interval = std::chrono::seconds((int)(std::uniform_real_distribution<>(fetch_interval_min, fetch_interval_max)(pubkeys_state.engine))); @@ -1043,23 +1043,11 @@ namespace nmos auto fetch_time = std::chrono::steady_clock::now(); return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() { - auto lock = authorization_state.read_lock(); - return details::request_jwks(*pubkeys_state.client, pubkeys_state.version, gate, token); }).then([&authorization_state, &pubkeys_state, &gate](web::json::value jwks_) { - web::uri issuer; - bool one_shot{ false }; - nmos::api_version auth_version; - nmos::with_read_lock(authorization_state.mutex, [&] - { - issuer = pubkeys_state.issuer; - one_shot = pubkeys_state.one_shot; - auth_version = pubkeys_state.version; - }); - - const auto jwks = nmos::experimental::get_jwks(authorization_state, issuer); + const auto jwks = nmos::experimental::get_jwks(authorization_state, pubkeys_state.issuer); // are changes found in new set of jwks? if(jwks != jwks_) @@ -1079,34 +1067,34 @@ namespace nmos } catch (const jwk_exception& e) { - slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(issuer.to_string()) << " JWK error: " << e.what(); + slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(pubkeys_state.issuer.to_string()) << " JWK error: " << e.what(); } } // update jwks and jwt validator cache if (pems.as_array().size()) { - nmos::experimental::update_jwks(authorization_state, issuer, jwks_, nmos::experimental::jwt_validator(pems, [auth_version](const web::json::value& payload) + nmos::experimental::update_jwks(authorization_state, pubkeys_state.issuer, jwks_, nmos::experimental::jwt_validator(pems, [&pubkeys_state](const web::json::value& payload) { // validate access token payload JSON - authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(auth_version)); // may throw json_exception + authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(pubkeys_state.version)); // may throw json_exception })); - slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys for " << utility::us2s(issuer.to_string()); + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys for " << utility::us2s(pubkeys_state.issuer.to_string()); } else { - nmos::experimental::erase_jwks(authorization_state, issuer); + nmos::experimental::erase_jwks(authorization_state, pubkeys_state.issuer); - slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list for " << utility::us2s(issuer.to_string()); + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list for " << utility::us2s(pubkeys_state.issuer.to_string()); } } else { - slog::log(gate, SLOG_FLF) << "No public keys changes found for " << utility::us2s(issuer.to_string()); + slog::log(gate, SLOG_FLF) << "No public keys changes found for " << utility::us2s(pubkeys_state.issuer.to_string()); } - return !one_shot; + return true; }); }).then([&](pplx::task finally) { @@ -1117,41 +1105,50 @@ namespace nmos finally.get(); nmos::with_write_lock(authorization_state.mutex, [&] { pubkeys_state.received = true; }); + authorization_service_error = false; } catch (const web::http::http_exception& e) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + authorization_service_error = true; } catch (const web::json::json_exception& e) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request JSON error: " << e.what(); + authorization_service_error = true; } catch (const web::http::oauth2::experimental::oauth2_exception& e) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request OAuth 2.0 error: " << e.what(); + authorization_service_error = true; } catch (const jwk_exception& e) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request JWK error: " << e.what(); + authorization_service_error = true; } catch (const std::exception& e) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request error: " << e.what(); + authorization_service_error = true; } catch (const authorization_exception&) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request error"; + authorization_service_error = true; } catch (...) { slog::log(gate, SLOG_FLF) << "Authorization API jwks request unexpected unknown exception"; + + authorization_service_error = true; } model.notify(); @@ -1713,14 +1710,12 @@ namespace nmos pplx::cancellation_token_source cancellation_source; auto pubkeys_requests(pplx::task_from_result()); - - with_write_lock(authorization_state.mutex, [&] + pubkeys_shared_state pubkeys_state { - auto& pubkeys_state = authorization_state.pubkeys_state; - pubkeys_state.client.reset(new web::http::client::http_client{ jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }); - pubkeys_state.version = authorization_version; - pubkeys_state.issuer = nmos::experimental::fields::issuer(authorization_server_metadata); - }); + { jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, + authorization_version, + nmos::experimental::fields::issuer(authorization_server_metadata) + }; auto bearer_token_requests(pplx::task_from_result()); web::http::oauth2::experimental::oauth2_token bearer_token; @@ -1745,7 +1740,7 @@ namespace nmos // start a background task to fetch public keys from authorization server if (nmos::experimental::fields::server_authorization(model.settings)) { - pubkeys_requests = do_public_keys_requests(model, authorization_state, authorization_state.pubkeys_state, authorization_service_error, gate, token); + pubkeys_requests = do_public_keys_requests(model, authorization_state, pubkeys_state, authorization_service_error, gate, token); } // start a background task to fetch bearer access token from authorization server @@ -1764,7 +1759,7 @@ namespace nmos bearer_token_requests.wait(); } - // make an asynchronously GET request over the Token Issuer to fetch issuer metadata + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch issuer metadata bool request_token_issuer_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { auto lock = model.write_lock(); @@ -1780,10 +1775,10 @@ namespace nmos // wait for the thread to be interrupted because of no matching public keys from the received token or because the server is being shut down condition.wait(lock, [&] { return shutdown || nmos::with_read_lock(authorization_state.mutex, [&] { return authorization_state.fetch_token_issuer_pubkeys; }); }); - if (shutdown) return false; - slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer metadata fetch"; + if (shutdown) return false; + const auto token_issuer = nmos::with_write_lock(authorization_state.mutex, [&] { authorization_state.fetch_token_issuer_pubkeys = false; @@ -1791,24 +1786,21 @@ namespace nmos }); if (token_issuer.is_empty()) { - slog::log(gate, SLOG_FLF) << "No authorization token's issuer to fetch server metadata"; + slog::log(gate, SLOG_FLF) << "No authorization token's issuer to fetch server metadata"; return false; } + web::http::client::http_client client(make_authorization_service_uri(token_issuer), make_authorization_http_client_config(model.settings, load_ca_certificates, gate)); const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); const auto grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); const auto token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); - slog::log(gate, SLOG_FLF) << "Using authorization token's issuer " << utility::us2s(token_issuer.to_string()) << " to fetch server metadata"; - - web::http::client::http_client client(make_authorization_service_uri(token_issuer), make_authorization_http_client_config(model.settings, load_ca_certificates, gate)); - auto token = cancellation_source.get_token(); - auto request = details::request_authorization_server_metadata(client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&, token_issuer](web::json::value metadata) + auto request = details::request_authorization_server_metadata(client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&authorization_state, token_issuer](web::json::value metadata) { - // cache the issuer metadata + // cache the token issuer(authorization server) metadata nmos::experimental::update_authorization_server_metadata(authorization_state, token_issuer, metadata); }).then([&](pplx::task finally) @@ -1847,7 +1839,7 @@ namespace nmos } catch (...) { - slog::log(gate, SLOG_FLF) << "Authorization API metadata request unexpected unknown exception"; + slog::log(gate, SLOG_FLF) << "Authorization API metadata unexpected unknown exception"; authorization_service_error = true; } @@ -1865,11 +1857,11 @@ namespace nmos nmos::details::reverse_lock_guard unlock{ lock }; request.wait(); - return !authorization_service_error && metadata_received; + return metadata_received; } - // make an asynchronously GET request over the Token Issuer to fetch public keys - void request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch public keys + bool request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer's public keys fetch"; @@ -1879,6 +1871,8 @@ namespace nmos bool authorization_service_error(false); + bool jwks_received(false); + pplx::cancellation_token_source cancellation_source; const auto token_issuer = with_read_lock(authorization_state.mutex, [&] { return authorization_state.token_issuer; }); @@ -1888,31 +1882,127 @@ namespace nmos pubkeys_shared_state pubkeys_state( { jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, authorization_version, - token_issuer, - true + token_issuer ); - // update the authorization_behaviour_thread's fetch public keys shared state, public keys are going to be fetched from this token issuer from now on - with_write_lock(authorization_state.mutex, [&] + auto token = cancellation_source.get_token(); + + auto request = details::request_jwks(*pubkeys_state.client, pubkeys_state.version, gate, token).then([&authorization_state, &pubkeys_state, &gate](web::json::value jwks_) { - auto& pubkeys_state = authorization_state.pubkeys_state; - pubkeys_state.client.reset(new web::http::client::http_client{ jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }); - pubkeys_state.version = authorization_version; - pubkeys_state.issuer = token_issuer; - }); + const auto jwks = nmos::experimental::get_jwks(authorization_state, pubkeys_state.issuer); - auto token = cancellation_source.get_token(); + // are changes found in new set of jwks? + if (jwks != jwks_) + { + // convert jwks to array of public keys + auto pems = web::json::value::array(); + for (const auto& jwk : jwks_.as_array()) + { + try + { + const auto pem = jwk_to_public_key(jwk); // can throw jwk_exception + + web::json::push_back(pems, web::json::value_of({ + { U("jwk"), jwk }, + { U("pem"), pem } + })); + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(pubkeys_state.issuer.to_string()) << " JWK error: " << e.what(); + } + } + + // update jwks and jwt validator cache + if (pems.as_array().size()) + { + nmos::experimental::update_jwks(authorization_state, pubkeys_state.issuer, jwks_, nmos::experimental::jwt_validator(pems, [&pubkeys_state](const web::json::value& payload) + { + // validate access token payload JSON + authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(pubkeys_state.version)); // may throw json_exception + })); + + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + else + { + nmos::experimental::erase_jwks(authorization_state, pubkeys_state.issuer); + + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + } + else + { + slog::log(gate, SLOG_FLF) << "No public keys changes found for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state - // start a one-shot background task to fetch public keys from the token issuer - auto pubkeys_requests = do_public_keys_requests(model, authorization_state, pubkeys_state, authorization_service_error, gate, token); + try + { + finally.get(); + + jwks_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request OAuth 2.0 error: " << e.what(); + + authorization_service_error = true; + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JWK error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request unexpected unknown exception"; + + authorization_service_error = true; + } + }); + request.then([&] + { + condition.notify_all(); + }); // wait for the request because interactions with the Authorization API endpoint must be sequential - condition.wait(lock, [&] { return shutdown || authorization_service_error || pubkeys_state.received; }); + condition.wait(lock, [&] { return shutdown || authorization_service_error || jwks_received; }); cancellation_source.cancel(); // wait without the lock since it is also used by the background tasks nmos::details::reverse_lock_guard unlock{ lock }; - pubkeys_requests.wait(); + request.wait(); + + return jwks_received; } } } diff --git a/Development/nmos/authorization_operation.h b/Development/nmos/authorization_operation.h index f3f95de04..eadc01c9d 100644 --- a/Development/nmos/authorization_operation.h +++ b/Development/nmos/authorization_operation.h @@ -68,11 +68,11 @@ namespace nmos // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate); - // make an asynchronously GET request over the Token Issuer to fetch issuer metadata + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch issuer metadata bool request_token_issuer_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); - // make an asynchronously GET request over the Token Issuer to fetch public keys - void request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch public keys + bool request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); } } } diff --git a/Development/nmos/authorization_state.h b/Development/nmos/authorization_state.h index 06d326344..9cf714198 100644 --- a/Development/nmos/authorization_state.h +++ b/Development/nmos/authorization_state.h @@ -20,22 +20,19 @@ namespace nmos std::unique_ptr client; nmos::api_version version; web::uri issuer; - bool one_shot; // how offen to fetch the public keys, where true = one-shot; false = loop based on time interval bool immediate; // true = do an immediate fetch; false = do time interval fetch bool received; pubkeys_shared_state() : engine(seeder) - , one_shot(false) , immediate(true) , received(false) {} - pubkeys_shared_state(web::http::client::http_client client, nmos::api_version version, web::uri issuer, bool one_shot = false) + pubkeys_shared_state(web::http::client::http_client client, nmos::api_version version, web::uri issuer)//, bool one_shot = false) : engine(seeder) , client(std::unique_ptr(new web::http::client::http_client(client))) , version(std::move(version)) , issuer(std::move(issuer)) - , one_shot(one_shot) , immediate(true) , received(false) {} }; @@ -73,9 +70,6 @@ namespace nmos // OAuth 2.0 bearer token to access authorizaton protected APIs web::http::oauth2::experimental::oauth2_token bearer_token; - // shared state for the public keys fetch - pubkeys_shared_state pubkeys_state; - nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } From 0a62a2c5dec595add4b343517389eb89610adcc9 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Mon, 9 Oct 2023 18:57:26 +0100 Subject: [PATCH 079/121] Down the HTTP_TIMEOUT to 2s --- .github/workflows/build-test.yml | 4 ++-- .github/workflows/src/amwa-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 9540e66df..c532316d0 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -345,7 +345,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 100\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -874,7 +874,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 100\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 9783b504e..f2e7aa06b 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -20,7 +20,7 @@ cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 100\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token From 657cb4c2d116e9aef6f163956ecfa8babccabb2f Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 10 Oct 2023 15:38:05 +0100 Subject: [PATCH 080/121] Bump up jwt-cpp to 0.6.0 --- Development/README.md | 2 - Development/cmake/NmosCppLibraries.cmake | 4 - Development/conanfile.txt | 2 +- Development/jwt/nlohmann_traits.h | 120 ---- Development/nmos/jwt_generator_impl.cpp | 6 +- Development/nmos/jwt_validator_impl.cpp | 26 +- Development/third_party/jwt-cpp/README.md | 149 ++--- Development/third_party/jwt-cpp/base.h | 301 ++++----- Development/third_party/jwt-cpp/jwt.h | 747 ++++++++++++---------- README.md | 2 + 10 files changed, 584 insertions(+), 775 deletions(-) delete mode 100644 Development/jwt/nlohmann_traits.h diff --git a/Development/README.md b/Development/README.md index c135ade07..c4464e9ec 100644 --- a/Development/README.md +++ b/Development/README.md @@ -14,8 +14,6 @@ C++ source code and build files for the software Small general purpose utilties and header files to facilitate cross-platform development - [jwk](jwk) An implementation of the conversion between JSON Web Key and public key -- [jwt](jwt) - Providing the nlohmann JSON traits for JSON Web Key support using jwt-cpp library - [lldp](lldp) A simple API for LLDP and an implementation using the PCAP *pcap.h* API - [mdns](mdns) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index b95f32236..197183869 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -826,10 +826,6 @@ set(NMOS_CPP_JWK_HEADERS jwk/public_key_use.h ) -set(NMOS_CPP_JWT_HEADERS - jwt/nlohmann_traits.h - ) - set(NMOS_CPP_NMOS_SOURCES nmos/activation_utils.cpp nmos/admin_ui.cpp diff --git a/Development/conanfile.txt b/Development/conanfile.txt index 6d0c58d31..01f151bf1 100644 --- a/Development/conanfile.txt +++ b/Development/conanfile.txt @@ -6,7 +6,7 @@ openssl/1.1.1s json-schema-validator/2.2.0 nlohmann_json/3.11.2 zlib/1.2.13 -jwt-cpp/0.5.1 +jwt-cpp/0.6.0 [imports] bin, *.dll -> ./bin diff --git a/Development/jwt/nlohmann_traits.h b/Development/jwt/nlohmann_traits.h deleted file mode 100644 index f15b7710a..000000000 --- a/Development/jwt/nlohmann_traits.h +++ /dev/null @@ -1,120 +0,0 @@ -#ifndef JWT_NLOHMANN_H -#define JWT_NLOHMANN_H - -#include "jwt-cpp/jwt.h" -#include "nlohmann/json.hpp" - -namespace jwt -{ - namespace experimental - { - namespace details - { - struct nlohmann_traits { - using json = nlohmann::json; - using value_type = json; - using object_type = json::object_t; - using array_type = json::array_t; - using string_type = std::string; // current limitation of traits implementation - using number_type = json::number_float_t; - using integer_type = json::number_integer_t; - using boolean_type = json::boolean_t; - - static jwt::json::type get_type(const json &val) - { - using jwt::json::type; - - if (val.type() == json::value_t::boolean) { return type::boolean; } - else if (val.type() == json::value_t::number_integer) { return type::integer; } - // nlohmann internally tracks two types of integers - else if (val.type() == json::value_t::number_unsigned) { return type::integer; } - else if (val.type() == json::value_t::number_float) { return type::number; } - else if (val.type() == json::value_t::string) { return type::string; } - else if (val.type() == json::value_t::array) { return type::array; } - else if (val.type() == json::value_t::object) { return type::object; } - else { throw std::logic_error("invalid type"); } - } - - static json::object_t as_object(const json &val) - { - if (val.type() != json::value_t::object) - { - throw std::bad_cast(); - } - return val.get(); - } - - static std::string as_string(const json &val) - { - if (val.type() != json::value_t::string) - { - throw std::bad_cast(); - } - return val.get(); - } - - static json::array_t as_array(const json &val) - { - if (val.type() != json::value_t::array) - { - throw std::bad_cast(); - } - return val.get(); - } - - static int64_t as_int(const json &val) - { - switch (val.type()) - { - case json::value_t::number_integer: - case json::value_t::number_unsigned: - return val.get(); - default: - throw std::bad_cast(); - } - } - - static int64_t as_integer(const json &val) - { - return as_int(val); - } - - static bool as_bool(const json &val) - { - if (val.type() != json::value_t::boolean) - { - throw std::bad_cast(); - } - return val.get(); - } - - static bool as_boolean(const json &val) - { - return as_bool(val); - } - - static double as_number(const json &val) - { - if (val.type() != json::value_t::number_float) - { - throw std::bad_cast(); - } - return val.get(); - } - - static bool parse(json &val, std::string str) - { - val = json::parse(str.begin(), str.end()); - return true; - } - - static std::string serialize(const json &val) - { - return val.dump(); - } - }; - } - } -} - -#endif diff --git a/Development/nmos/jwt_generator_impl.cpp b/Development/nmos/jwt_generator_impl.cpp index f063fd2c3..ee6730214 100644 --- a/Development/nmos/jwt_generator_impl.cpp +++ b/Development/nmos/jwt_generator_impl.cpp @@ -1,7 +1,7 @@ #include "nmos/jwt_generator.h" +#include #include "cpprest/basic_utils.h" -#include "jwt/nlohmann_traits.h" #include "nmos/id.h" #include "nmos/jwk_utils.h" @@ -16,13 +16,13 @@ namespace nmos public: static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& public_key, const utility::string_t& private_key, const utility::string_t& keyid) { - using namespace jwt::experimental::details; + using namespace jwt::traits; // use server private key to create client_assertion (JWT) // where client_assertion MUST including iss, sub, aud, exp, and may including jti // see https://tools.ietf.org/html/rfc7523#section-2.2 // see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication - return utility::s2us(jwt::create() + return utility::s2us(jwt::create() .set_issuer(utility::us2s(issuer)) .set_subject(utility::us2s(subject)) .set_audience(utility::us2s(audience.to_string())) diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index 7f8fbb659..e788de200 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -1,12 +1,12 @@ #include "nmos/jwt_validator.h" #include +#include #include "cpprest/basic_utils.h" #include "cpprest/http_msg.h" #include "cpprest/json.h" #include "cpprest/regex_utils.h" #include "cpprest/uri_schemes.h" -#include "jwt/nlohmann_traits.h" #include "nmos/authorization_utils.h" #include "nmos/json_fields.h" @@ -22,7 +22,7 @@ namespace nmos explicit jwt_validator_impl(const web::json::value& pubkeys, token_validator token_validation) : token_validation(token_validation) { - using namespace jwt::experimental::details; + using namespace jwt::traits; if (pubkeys.is_array()) { @@ -59,7 +59,7 @@ namespace nmos if (U("RS512") != jwk.at(U("alg")).as_string()) continue; } - auto validator = jwt::verify({}); + auto validator = jwt::verify({}); try { validator.allow_algorithm(jwt::algorithm::rs512(utility::us2s(pubkey.at(U("pem")).as_string()))); @@ -75,10 +75,10 @@ namespace nmos void validate_expiry(const utility::string_t& token) const { - using namespace jwt::experimental::details; + using namespace jwt::traits; // verify JWT is well formed - auto decoded_token = jwt::decode(utility::us2s(token)); + auto decoded_token = jwt::decode(utility::us2s(token)); for (const auto& validator : validators) { @@ -102,10 +102,10 @@ namespace nmos void validate(const utility::string_t& token, const web::http::http_request& req, const scope& scope, const utility::string_t& audience) const { - using namespace jwt::experimental::details; + using namespace jwt::traits; // verify JWT is well formed - auto decoded_token = jwt::decode(utility::us2s(token)); + auto decoded_token = jwt::decode(utility::us2s(token)); // validate bearer token payload JSON if (token_validation) @@ -250,7 +250,7 @@ namespace nmos { if (x_nmos_scope_claim.contains(access_right)) { - auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); + auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); for (auto& accessible_path : accessible_paths) { // construct path regex for regex comparison @@ -375,10 +375,10 @@ namespace nmos // may throw static utility::string_t get_client_id(const utility::string_t& token) { - using namespace jwt::experimental::details; + using namespace jwt::traits; // verify JWT is well formed - auto decoded_token = jwt::decode(utility::us2s(token)); + auto decoded_token = jwt::decode(utility::us2s(token)); // token does not guarantee to have client_id // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#client_id if (decoded_token.has_payload_claim("client_id")) @@ -403,10 +403,10 @@ namespace nmos // may throw static web::uri get_token_issuer(const utility::string_t& token) { - using namespace jwt::experimental::details; + using namespace jwt::traits; // verify JWT is well formed - auto decoded_token = jwt::decode(utility::us2s(token)); + auto decoded_token = jwt::decode(utility::us2s(token)); return utility::s2us(decoded_token.get_issuer()); } @@ -424,7 +424,7 @@ namespace nmos } private: - std::vector> validators; + std::vector> validators; token_validator token_validation; }; } diff --git a/Development/third_party/jwt-cpp/README.md b/Development/third_party/jwt-cpp/README.md index 5e3903262..0f4f365d9 100644 --- a/Development/third_party/jwt-cpp/README.md +++ b/Development/third_party/jwt-cpp/README.md @@ -1,4 +1,4 @@ -# ![logo](https://raw.githubusercontent.com/Thalhammer/jwt-cpp/master/.github/logo.svg) +logo [![License Badge](https://img.shields.io/github/license/Thalhammer/jwt-cpp)](https://github.com/Thalhammer/jwt-cpp/blob/master/LICENSE) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5f7055e294744901991fd0a1620b231d)](https://app.codacy.com/app/Thalhammer/jwt-cpp?utm_source=github.com&utm_medium=referral&utm_content=Thalhammer/jwt-cpp&utm_campaign=Badge_Grade_Settings) @@ -6,9 +6,13 @@ [![MacOS Badge][MacOS]][Cross-Platform] [![Windows Badge][Windows]][Cross-Platform] [![Coverage Status](https://coveralls.io/repos/github/Thalhammer/jwt-cpp/badge.svg?branch=master)](https://coveralls.io/github/Thalhammer/jwt-cpp?branch=master) + [![Documentation Badge](https://img.shields.io/badge/Documentation-master-blue)](https://thalhammer.github.io/jwt-cpp/) -[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/Thalhammer/jwt-cpp?include_prereleases)](https://github.com/Thalhammer/jwt-cpp/releases) + [![Stars Badge](https://img.shields.io/github/stars/Thalhammer/jwt-cpp)](https://github.com/Thalhammer/jwt-cpp/stargazers) +[![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/Thalhammer/jwt-cpp?include_prereleases)](https://github.com/Thalhammer/jwt-cpp/releases) +[![ConanCenter package](https://repology.org/badge/version-for-repo/conancenter/jwt-cpp.svg)](https://repology.org/project/jwt-cpp/versions) +[![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/jwt-cpp.svg)](https://repology.org/project/jwt-cpp/versions) [Linux]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/cross-platform/ubuntu-latest/shields.json [MacOS]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/cross-platform/macos-latest/shields.json @@ -23,27 +27,35 @@ jwt-cpp supports all the algorithms defined by the specifications. The modular d For completeness, here is a list of all supported algorithms: -| HMSC | RSA | ECDSA | PSS | EdDSA | -| ----- | ----- | ----- | ----- | ------- | -| HS256 | RS256 | ES256 | PS256 | Ed25519 | -| HS384 | RS384 | ES384 | PS384 | Ed448 | -| HS512 | RS512 | ES512 | PS512 | | +| HMSC | RSA | ECDSA | PSS | EdDSA | +|-------|-------|--------|-------|---------| +| HS256 | RS256 | ES256 | PS256 | Ed25519 | +| HS384 | RS384 | ES384 | PS384 | Ed448 | +| HS512 | RS512 | ES512 | PS512 | | +| | | ES256K | | | ## SSL Compatibility -In the name of flexibility and extensibility, jwt-cpp supports both [OpenSSL](https://github.com/openssl/openssl) and [LibreSSL](https://github.com/libressl-portable/portable). These are the version which are, or have been, tested: +In the name of flexibility and extensibility, jwt-cpp supports [OpenSSL](https://github.com/openssl/openssl), [LibreSSL](https://github.com/libressl-portable/portable), and [wolfSSL](https://github.com/wolfSSL/wolfssl). Read [this page](docs/ssl.md) for more details. These are the version which are currently being tested: -| OpenSSL | LibreSSL | -| -------------- | --------------- | -| [1.0.2][1.0.2] | ![3.1.5][3.1] | -| 1.1.0 | ![3.2.3][3.2] | -| [1.1.1][1.1.1] | ![3.3.1][3.3] | +| OpenSSL | LibreSSL | wolfSSL | +|-------------------|----------------|----------------| +| ![1.0.2u][o1.0.2] | ![3.2.7][l3.2] | ![5.0.0][w5.0] | +| ![1.1.0i][o1.1.0] | ![3.3.5][l3.3] | ![5.1.1][w5.1] | +| ![1.1.1m][o1.1.1] | ![3.4.2][l3.4] | | +| ![3.0.1][o3.0] | | | -[1.0.2]: https://travis-ci.com/github/Thalhammer/jwt-cpp -[1.1.1]: https://github.com/Thalhammer/jwt-cpp/actions?query=workflow%3A%22Coverage+CI%22 -[3.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.1.5/shields.json -[3.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.2.3/shields.json -[3.3]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.3.1/shields.json +> ℹ️ Note: A complete list of versions tested in the past can be found [here](https://github.com/Thalhammer/jwt-cpp/tree/badges). + +[o1.0.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.0.2u/shields.json +[o1.1.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.1.0i/shields.json +[o1.1.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.1.1m/shields.json +[o3.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/3.0.1/shields.json +[l3.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.2.7/shields.json +[l3.3]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.3.5/shields.json +[l3.4]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.4.2/shields.json +[w5.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/wolfssl/5.0.0/shields.json +[w5.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/wolfssl/5.1.1/shields.json ## Overview @@ -53,9 +65,21 @@ There is no hard dependency on a JSON library. Instead, there's a generic `jwt:: jwt::basic_claim claim(json::object({{"json", true},{"example", 0}})); ``` -This allows for complete freedom when picking which libraries you want to use. For more information, [see below](#providing-your-own-json-traits-your-traits). +This allows for complete freedom when picking which libraries you want to use. For more information, [read this page](docs/traits.md)). + +For your convience there are serval traits implementation which provide some popular JSON libraries. They are: + +[![picojson][picojson]](https://github.com/kazuho/picojson) +[![nlohmann][nlohmann]](https://github.com/nlohmann/json) +[![jsoncons][jsoncons]](https://github.com/danielaparker/jsoncons) +[![boostjson][boostjson]](https://github.com/boostorg/json) -In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. +[picojson]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/kazuho-picojson/shields.json +[nlohmann]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/nlohmann-json/shields.json +[jsoncons]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/danielaparker-jsoncons/shields.json +[boostjson]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/boost-json/shields.json + +In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. It's possible to directly include the traits defaults for the other JSON libraries. See the [traits examples](https://github.com/prince-chrismc/jwt-cpp/tree/master/example/traits) for details. As for the base64 requirements of JWTs, this libary provides `base.h` with all the required implentation; However base64 implementations are very common, with varying degrees of performance. When providing your own base64 implementation, you can define `JWT_DISABLE_BASE64` to remove the jwt-cpp implementation. @@ -98,74 +122,11 @@ auto token = jwt::create() .sign(jwt::algorithm::hs256{"secret"}); ``` -Here is a simple example of creating a token that will expire in one hour: - -```cpp -auto token = jwt::create() - .set_issuer("auth0") - .set_issued_at(std::chrono::system_clock::now()) - .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{3600}) - .sign(jwt::algorithm::hs256{"secret"}); -``` - > To see more examples working with RSA public and private keys, visit our [examples](https://github.com/Thalhammer/jwt-cpp/tree/master/example)! ### Providing your own JSON Traits -There are several key items that need to be provided to a `jwt::basic_claim` in order for it to be interoptable with you JSON library of choice. - -* type specifications -* conversion from generic "value type" to a specific type -* serialization and parsing - -If ever you are not sure, the traits are heavily checked against static asserts to make sure you provide everything that's required. - -> :warning: Not all JSON libraries are a like, you may need to extent certain types such that it can be used by jwt-cpp. See this [example](https://github.com/Thalhammer/jwt-cpp/blob/ac3de9e69bc698a464dacb256a1b50512843f092/tests/jsoncons/JsonconsTest.cpp). - -```cpp -struct my_favorite_json_library_traits { - // Type Specifications - using value_type = json; // The generic "value type" implementation, most libraries have one - using object_type = json::object_t; // The "map type" string to value - using array_type = json::array_t; // The "list type" array of values - using string_type = std::string; // The "list of chars", must be a narrow char - using number_type = double; // The "percision type" - using integer_type = int64_t; // The "integral type" - using boolean_type = bool; // The "boolean type" - - // Translation between the implementation notion of type, to the jwt::json::type equivilant - static jwt::json::type get_type(const value_type &val) { - using jwt::json::type; - - if (val.type() == json::value_t::object) - return type::object; - if (val.type() == json::value_t::array) - return type::array; - if (val.type() == json::value_t::string) - return type::string; - if (val.type() == json::value_t::number_float) - return type::number; - if (val.type() == json::value_t::number_integer) - return type::integer; - if (val.type() == json::value_t::boolean) - return type::boolean; - - throw std::logic_error("invalid type"); - } - - // Conversion from generic value to specific type - static object_type as_object(const value_type &val); - static array_type as_array(const value_type &val); - static string_type as_string(const value_type &val); - static number_type as_number(const value_type &val); - static integer_type as_int(const value_type &val); - static boolean_type as_bool(const value_type &val); - - // serilization and parsing - static bool parse(value_type &val, string_type str); - static string_type serialize(const value_type &val); // with no extra whitespace, padding or indentation -}; -``` +To learn how to writes a trait's implementation, checkout the [these instructions](docs/traits.md) ## Contributing @@ -187,22 +148,4 @@ In order to build the test cases you also need ## Troubleshooting -### Expired tokens - -If you are generating tokens that seem to immediately expire, you are likely not using UTC. Specifically, -if you use `get_time` to get the current time, it likely uses localtime, while this library uses UTC, -which may be why your token is immediately expiring. Please see example above on the right way to use current time. - -### Missing \_HMAC and \_EVP_sha256 symbols on Mac - -There seems to exists a problem with the included openssl library of MacOS. Make sure you link to one provided by brew. -See [here](https://github.com/Thalhammer/jwt-cpp/issues/6) for more details. - -### Building on windows fails with syntax errors - -The header ``, which is often included in windowsprojects, defines macros for MIN and MAX which screw up std::numeric_limits. -See [here](https://github.com/Thalhammer/jwt-cpp/issues/5) for more details. To fix this do one of the following things: - -* define NOMINMAX, which suppresses this behaviour -* include this library before you include windows.h -* place `#undef max` and `#undef min` before you include this library +See the [FAQs](docs/faqs.md) for tips. diff --git a/Development/third_party/jwt-cpp/base.h b/Development/third_party/jwt-cpp/base.h index cef493d19..4177ed68d 100644 --- a/Development/third_party/jwt-cpp/base.h +++ b/Development/third_party/jwt-cpp/base.h @@ -1,11 +1,9 @@ #ifndef JWT_CPP_BASE_H #define JWT_CPP_BASE_H -#include #include #include #include -#include #ifdef __has_cpp_attribute #if __has_cpp_attribute(fallthrough) @@ -23,10 +21,7 @@ namespace jwt { */ namespace alphabet { /** - * \brief valid list of character when working with [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4) - * - * As directed in [X.509 Parameter](https://datatracker.ietf.org/doc/html/rfc7517#section-4.7) certificate chains are - * base64-encoded as per [Section 4 of RFC4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4) + * \brief valid list of characted when working with [Base64](https://tools.ietf.org/html/rfc3548) */ struct base64 { static const std::array& data() { @@ -43,13 +38,7 @@ namespace jwt { } }; /** - * \brief valid list of character when working with [Base64URL](https://tools.ietf.org/html/rfc4648#section-5) - * - * As directed by [RFC 7519 Terminology](https://datatracker.ietf.org/doc/html/rfc7519#section-2) set the definition of Base64URL - * encoding as that in [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-2) that states: - * - * > Base64 encoding using the URL- and filename-safe character set defined in - * > [Section 5 of RFC 4648 RFC4648](https://tools.ietf.org/html/rfc4648#section-5), with all trailing '=' characters omitted + * \brief valid list of characted when working with [Base64URL](https://tools.ietf.org/html/rfc4648) */ struct base64url { static const std::array& data() { @@ -65,205 +54,155 @@ namespace jwt { return fill; } }; - namespace helper { - /** - * @brief A General purpose base64url alphabet respecting the - * [URI Case Normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1) - * - * This is useful in situations outside of JWT encoding/decoding and is provided as a helper - */ - struct base64url_percent_encoding { - static const std::array& data() { - static constexpr std::array data{ - {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', - 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; - return data; - } - static const std::initializer_list& fill() { - static std::initializer_list fill{"%3D", "%3d"}; - return fill; - } - }; - } // namespace helper - - inline uint32_t index(const std::array& alphabet, char symbol) { - auto itr = std::find_if(alphabet.cbegin(), alphabet.cend(), [symbol](char c) { return c == symbol; }); - if (itr == alphabet.cend()) { throw std::runtime_error("Invalid input: not within alphabet"); } - - return std::distance(alphabet.cbegin(), itr); - } } // namespace alphabet /** - * \brief A collection of fellable functions for working with base64 and base64url + * \brief Alphabet generic methods for working with encoding/decoding the base64 family */ - namespace base { - - namespace details { - struct padding { - size_t count = 0; - size_t length = 0; - - padding() = default; - padding(size_t count, size_t length) : count(count), length(length) {} + class base { + public: + template + static std::string encode(const std::string& bin) { + return encode(bin, T::data(), T::fill()); + } + template + static std::string decode(const std::string& base) { + return decode(base, T::data(), T::fill()); + } + template + static std::string pad(const std::string& base) { + return pad(base, T::fill()); + } + template + static std::string trim(const std::string& base) { + return trim(base, T::fill()); + } - padding operator+(const padding& p) { return padding(count + p.count, length + p.length); } + private: + static std::string encode(const std::string& bin, const std::array& alphabet, + const std::string& fill) { + size_t size = bin.size(); + std::string res; - friend bool operator==(const padding& lhs, const padding& rhs) { - return lhs.count == rhs.count && lhs.length == rhs.length; - } - }; + // clear incomplete bytes + size_t fast_size = size - size % 3; + for (size_t i = 0; i < fast_size;) { + uint32_t octet_a = static_cast(bin[i++]); + uint32_t octet_b = static_cast(bin[i++]); + uint32_t octet_c = static_cast(bin[i++]); - inline padding count_padding(const std::string& base, const std::vector& fills) { - for (const auto& fill : fills) { - if (base.size() < fill.size()) continue; - // Does the end of the input exactly match the fill pattern? - if (base.substr(base.size() - fill.size()) == fill) { - return padding{1, fill.length()} + - count_padding(base.substr(0, base.size() - fill.size()), fills); - } - } + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; - return {}; + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += alphabet[(triple >> 0 * 6) & 0x3F]; } - inline std::string encode(const std::string& bin, const std::array& alphabet, - const std::string& fill) { - size_t size = bin.size(); - std::string res; - - // clear incomplete bytes - size_t fast_size = size - size % 3; - for (size_t i = 0; i < fast_size;) { - uint32_t octet_a = static_cast(bin[i++]); - uint32_t octet_b = static_cast(bin[i++]); - uint32_t octet_c = static_cast(bin[i++]); - - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; - - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += alphabet[(triple >> 1 * 6) & 0x3F]; - res += alphabet[(triple >> 0 * 6) & 0x3F]; - } - - if (fast_size == size) return res; - - size_t mod = size % 3; - - uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; - uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; - uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; + if (fast_size == size) return res; + + size_t mod = size % 3; + + uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + switch (mod) { + case 1: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += fill; + res += fill; + break; + case 2: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += fill; + break; + default: break; + } - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + return res; + } - switch (mod) { - case 1: - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += fill; - res += fill; + static std::string decode(const std::string& base, const std::array& alphabet, + const std::string& fill) { + size_t size = base.size(); + + size_t fill_cnt = 0; + while (size > fill.size()) { + if (base.substr(size - fill.size(), fill.size()) == fill) { + fill_cnt++; + size -= fill.size(); + if (fill_cnt > 2) throw std::runtime_error("Invalid input: too much fill"); + } else break; - case 2: - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += alphabet[(triple >> 1 * 6) & 0x3F]; - res += fill; - break; - default: break; - } - - return res; } - inline std::string decode(const std::string& base, const std::array& alphabet, - const std::vector& fill) { - const auto pad = count_padding(base, fill); - if (pad.count > 2) throw std::runtime_error("Invalid input: too much fill"); + if ((size + fill_cnt) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size"); - const size_t size = base.size() - pad.length; - if ((size + pad.count) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size"); + size_t out_size = size / 4 * 3; + std::string res; + res.reserve(out_size); - size_t out_size = size / 4 * 3; - std::string res; - res.reserve(out_size); - - auto get_sextet = [&](size_t offset) { return alphabet::index(alphabet, base[offset]); }; - - size_t fast_size = size - size % 4; - for (size_t i = 0; i < fast_size;) { - uint32_t sextet_a = get_sextet(i++); - uint32_t sextet_b = get_sextet(i++); - uint32_t sextet_c = get_sextet(i++); - uint32_t sextet_d = get_sextet(i++); - - uint32_t triple = - (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); - - res += static_cast((triple >> 2 * 8) & 0xFFU); - res += static_cast((triple >> 1 * 8) & 0xFFU); - res += static_cast((triple >> 0 * 8) & 0xFFU); + auto get_sextet = [&](size_t offset) { + for (size_t i = 0; i < alphabet.size(); i++) { + if (alphabet[i] == base[offset]) return static_cast(i); } + throw std::runtime_error("Invalid input: not within alphabet"); + }; - if (pad.count == 0) return res; - - uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); + size_t fast_size = size - size % 4; + for (size_t i = 0; i < fast_size;) { + uint32_t sextet_a = get_sextet(i++); + uint32_t sextet_b = get_sextet(i++); + uint32_t sextet_c = get_sextet(i++); + uint32_t sextet_d = get_sextet(i++); - switch (pad.count) { - case 1: - triple |= (get_sextet(fast_size + 2) << 1 * 6); - res += static_cast((triple >> 2 * 8) & 0xFFU); - res += static_cast((triple >> 1 * 8) & 0xFFU); - break; - case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; - default: break; - } + uint32_t triple = (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); - return res; + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + res += static_cast((triple >> 0 * 8) & 0xFFU); } - inline std::string decode(const std::string& base, const std::array& alphabet, - const std::string& fill) { - return decode(base, alphabet, std::vector{fill}); - } + if (fill_cnt == 0) return res; - inline std::string pad(const std::string& base, const std::string& fill) { - std::string padding; - switch (base.size() % 4) { - case 1: padding += fill; JWT_FALLTHROUGH; - case 2: padding += fill; JWT_FALLTHROUGH; - case 3: padding += fill; JWT_FALLTHROUGH; - default: break; - } + uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); - return base + padding; + switch (fill_cnt) { + case 1: + triple |= (get_sextet(fast_size + 2) << 1 * 6); + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + break; + case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; + default: break; } - inline std::string trim(const std::string& base, const std::string& fill) { - auto pos = base.find(fill); - return base.substr(0, pos); + return res; + } + + static std::string pad(const std::string& base, const std::string& fill) { + std::string padding; + switch (base.size() % 4) { + case 1: padding += fill; JWT_FALLTHROUGH; + case 2: padding += fill; JWT_FALLTHROUGH; + case 3: padding += fill; JWT_FALLTHROUGH; + default: break; } - } // namespace details - template - std::string encode(const std::string& bin) { - return details::encode(bin, T::data(), T::fill()); + return base + padding; } - template - std::string decode(const std::string& base) { - return details::decode(base, T::data(), T::fill()); - } - template - std::string pad(const std::string& base) { - return details::pad(base, T::fill()); - } - template - std::string trim(const std::string& base) { - return details::trim(base, T::fill()); + + static std::string trim(const std::string& base, const std::string& fill) { + auto pos = base.find(fill); + return base.substr(0, pos); } - } // namespace base + }; } // namespace jwt #endif diff --git a/Development/third_party/jwt-cpp/jwt.h b/Development/third_party/jwt-cpp/jwt.h index ec63b64c7..211305e01 100644 --- a/Development/third_party/jwt-cpp/jwt.h +++ b/Development/third_party/jwt-cpp/jwt.h @@ -18,12 +18,11 @@ #include #include #include -#include #include #include #include -#include +#include #include #include #include @@ -35,10 +34,6 @@ #include #include -#if __cplusplus > 201103L -#include -#endif - #if __cplusplus >= 201402L #ifdef __has_include #if __has_include() @@ -58,12 +53,8 @@ #endif #if defined(LIBRESSL_VERSION_NUMBER) -#if LIBRESSL_VERSION_NUMBER >= 0x3050300fL -#define JWT_OPENSSL_1_1_0 -#else #define JWT_OPENSSL_1_0_0 #endif -#endif #if defined(LIBWOLFSSL_VERSION_HEX) #define JWT_OPENSSL_1_1_1 @@ -362,8 +353,15 @@ namespace jwt { } } } // namespace error -} // namespace jwt + // FIXME: Remove + // Keep backward compat at least for a couple of revisions + using error::ecdsa_exception; + using error::rsa_exception; + using error::signature_generation_exception; + using error::signature_verification_exception; + using error::token_verification_exception; +} // namespace jwt namespace std { template<> struct is_error_code_enum : true_type {}; @@ -376,7 +374,6 @@ namespace std { template<> struct is_error_code_enum : true_type {}; } // namespace std - namespace jwt { /** * \brief A collection for working with certificates @@ -386,119 +383,24 @@ namespace jwt { * you maybe need to extract the modulus and exponent of an RSA Public Key. */ namespace helper { - /** - * \brief Handle class for EVP_PKEY structures - * - * Starting from OpenSSL 1.1.0, EVP_PKEY has internal reference counting. This handle class allows - * jwt-cpp to leverage that and thus safe an allocation for the control block in std::shared_ptr. - * The handle uses shared_ptr as a fallback on older versions. The behaviour should be identical between both. - */ - class evp_pkey_handle { - public: - constexpr evp_pkey_handle() noexcept = default; -#ifdef JWT_OPENSSL_1_0_0 - /** - * \brief Construct a new handle. The handle takes ownership of the key. - * \param key The key to store - */ - explicit evp_pkey_handle(EVP_PKEY* key) { m_key = std::shared_ptr(key, EVP_PKEY_free); } - - EVP_PKEY* get() const noexcept { return m_key.get(); } - bool operator!() const noexcept { return m_key == nullptr; } - explicit operator bool() const noexcept { return m_key != nullptr; } - - private: - std::shared_ptr m_key{nullptr}; -#else - /** - * \brief Construct a new handle. The handle takes ownership of the key. - * \param key The key to store - */ - explicit constexpr evp_pkey_handle(EVP_PKEY* key) noexcept : m_key{key} {} - evp_pkey_handle(const evp_pkey_handle& other) : m_key{other.m_key} { - if (m_key != nullptr && EVP_PKEY_up_ref(m_key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); - } -// C++11 requires the body of a constexpr constructor to be empty -#if __cplusplus >= 201402L - constexpr -#endif - evp_pkey_handle(evp_pkey_handle&& other) noexcept - : m_key{other.m_key} { - other.m_key = nullptr; - } - evp_pkey_handle& operator=(const evp_pkey_handle& other) { - if (&other == this) return *this; - decrement_ref_count(m_key); - m_key = other.m_key; - increment_ref_count(m_key); - return *this; - } - evp_pkey_handle& operator=(evp_pkey_handle&& other) noexcept { - if (&other == this) return *this; - decrement_ref_count(m_key); - m_key = other.m_key; - other.m_key = nullptr; - return *this; - } - evp_pkey_handle& operator=(EVP_PKEY* key) { - decrement_ref_count(m_key); - m_key = key; - increment_ref_count(m_key); - return *this; - } - ~evp_pkey_handle() noexcept { decrement_ref_count(m_key); } - - EVP_PKEY* get() const noexcept { return m_key; } - bool operator!() const noexcept { return m_key == nullptr; } - explicit operator bool() const noexcept { return m_key != nullptr; } - - private: - EVP_PKEY* m_key{nullptr}; - - static void increment_ref_count(EVP_PKEY* key) { - if (key != nullptr && EVP_PKEY_up_ref(key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); - } - static void decrement_ref_count(EVP_PKEY* key) noexcept { - if (key != nullptr) EVP_PKEY_free(key); - } -#endif - }; - - inline std::unique_ptr make_mem_buf_bio() { - return std::unique_ptr(BIO_new(BIO_s_mem()), BIO_free_all); - } - - inline std::unique_ptr make_mem_buf_bio(const std::string& data) { - return std::unique_ptr( -#if OPENSSL_VERSION_NUMBER <= 0x10100003L - BIO_new_mem_buf(const_cast(data.data()), static_cast(data.size())), BIO_free_all -#else - BIO_new_mem_buf(data.data(), static_cast(data.size())), BIO_free_all -#endif - ); - } - - inline std::unique_ptr make_evp_md_ctx() { - return -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr(EVP_MD_CTX_create(), &EVP_MD_CTX_destroy); -#else - std::unique_ptr(EVP_MD_CTX_new(), &EVP_MD_CTX_free); -#endif - } - /** * \brief Extract the public key of a pem certificate * * \param certstr String containing the certificate encoded as pem * \param pw Password used to decrypt certificate (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error ocurred) + * \param ec error_code for error_detection (gets cleared if no error occures) */ inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw, std::error_code& ec) { ec.clear(); - auto certbio = make_mem_buf_bio(certstr); - auto keybio = make_mem_buf_bio(); +#if OPENSSL_VERSION_NUMBER <= 0x10100003L + std::unique_ptr certbio( + BIO_new_mem_buf(const_cast(certstr.data()), static_cast(certstr.size())), BIO_free_all); +#else + std::unique_ptr certbio( + BIO_new_mem_buf(certstr.data(), static_cast(certstr.size())), BIO_free_all); +#endif + std::unique_ptr keybio(BIO_new(BIO_s_mem()), BIO_free_all); if (!certbio || !keybio) { ec = error::rsa_error::create_mem_bio_failed; return {}; @@ -565,7 +467,7 @@ namespace jwt { std::unique_ptr cert( d2i_X509(NULL, &c_str, static_cast(decodedStr.size())), X509_free); - auto certbio = make_mem_buf_bio(); + std::unique_ptr certbio(BIO_new(BIO_s_mem()), BIO_free_all); if (!cert || !certbio) { ec = error::rsa_error::create_mem_bio_failed; return {}; @@ -649,34 +551,38 @@ namespace jwt { * \param password Password used to decrypt certificate (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password, - std::error_code& ec) { + inline std::shared_ptr load_public_key_from_string(const std::string& key, + const std::string& password, std::error_code& ec) { ec.clear(); - auto pubkey_bio = make_mem_buf_bio(); + std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); if (!pubkey_bio) { ec = error::rsa_error::create_mem_bio_failed; - return {}; + return nullptr; } if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { auto epkey = helper::extract_pubkey_from_cert(key, password, ec); - if (ec) return {}; + if (ec) return nullptr; const int len = static_cast(epkey.size()); if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return {}; + return nullptr; } } else { const int len = static_cast(key.size()); if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return {}; + return nullptr; } } - evp_pkey_handle pkey(PEM_read_bio_PUBKEY( - pubkey_bio.get(), nullptr, nullptr, - (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` - if (!pkey) ec = error::rsa_error::load_key_bio_read; + std::shared_ptr pkey( + PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, + (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` + EVP_PKEY_free); + if (!pkey) { + ec = error::rsa_error::load_key_bio_read; + return nullptr; + } return pkey; } @@ -689,7 +595,8 @@ namespace jwt { * \param password Password used to decrypt certificate or key (leave empty if not encrypted) * \throw rsa_exception if an error occurred */ - inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password = "") { + inline std::shared_ptr load_public_key_from_string(const std::string& key, + const std::string& password = "") { std::error_code ec; auto res = load_public_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -703,21 +610,25 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password, - std::error_code& ec) { - auto privkey_bio = make_mem_buf_bio(); + inline std::shared_ptr + load_private_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { + std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); if (!privkey_bio) { ec = error::rsa_error::create_mem_bio_failed; - return {}; + return nullptr; } const int len = static_cast(key.size()); if (BIO_write(privkey_bio.get(), key.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return {}; + return nullptr; + } + std::shared_ptr pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), + EVP_PKEY_free); + if (!pkey) { + ec = error::rsa_error::load_key_bio_read; + return nullptr; } - evp_pkey_handle pkey( - PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); - if (!pkey) ec = error::rsa_error::load_key_bio_read; return pkey; } @@ -728,7 +639,8 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \throw rsa_exception if an error occurred */ - inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password = "") { + inline std::shared_ptr load_private_key_from_string(const std::string& key, + const std::string& password = "") { std::error_code ec; auto res = load_private_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -744,34 +656,38 @@ namespace jwt { * \param password Password used to decrypt certificate (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, const std::string& password, - std::error_code& ec) { + inline std::shared_ptr + load_public_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { ec.clear(); - auto pubkey_bio = make_mem_buf_bio(); + std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); if (!pubkey_bio) { ec = error::ecdsa_error::create_mem_bio_failed; - return {}; + return nullptr; } if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { auto epkey = helper::extract_pubkey_from_cert(key, password, ec); - if (ec) return {}; + if (ec) return nullptr; const int len = static_cast(epkey.size()); if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { ec = error::ecdsa_error::load_key_bio_write; - return {}; + return nullptr; } } else { const int len = static_cast(key.size()); if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { ec = error::ecdsa_error::load_key_bio_write; - return {}; + return nullptr; } } - evp_pkey_handle pkey(PEM_read_bio_PUBKEY( - pubkey_bio.get(), nullptr, nullptr, - (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` - if (!pkey) ec = error::ecdsa_error::load_key_bio_read; + std::shared_ptr pkey( + PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, + (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` + EVP_PKEY_free); + if (!pkey) { + ec = error::ecdsa_error::load_key_bio_read; + return nullptr; + } return pkey; } @@ -784,8 +700,8 @@ namespace jwt { * \param password Password used to decrypt certificate or key (leave empty if not encrypted) * \throw ecdsa_exception if an error occurred */ - inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, - const std::string& password = "") { + inline std::shared_ptr load_public_ec_key_from_string(const std::string& key, + const std::string& password = "") { std::error_code ec; auto res = load_public_ec_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -799,21 +715,25 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, const std::string& password, - std::error_code& ec) { - auto privkey_bio = make_mem_buf_bio(); + inline std::shared_ptr + load_private_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { + std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); if (!privkey_bio) { ec = error::ecdsa_error::create_mem_bio_failed; - return {}; + return nullptr; } const int len = static_cast(key.size()); if (BIO_write(privkey_bio.get(), key.data(), len) != len) { ec = error::ecdsa_error::load_key_bio_write; - return {}; + return nullptr; + } + std::shared_ptr pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), + EVP_PKEY_free); + if (!pkey) { + ec = error::ecdsa_error::load_key_bio_read; + return nullptr; } - evp_pkey_handle pkey( - PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); - if (!pkey) ec = error::ecdsa_error::load_key_bio_read; return pkey; } @@ -824,8 +744,8 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \throw ecdsa_exception if an error occurred */ - inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, - const std::string& password = "") { + inline std::shared_ptr load_private_ec_key_from_string(const std::string& key, + const std::string& password = "") { std::error_code ec; auto res = load_private_ec_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -986,7 +906,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw error::rsa_exception(error::rsa_error::no_key_provided); + throw rsa_exception(error::rsa_error::no_key_provided); } /** * Sign jwt data @@ -996,7 +916,11 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - auto ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); +#endif if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1029,7 +953,11 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); - auto ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); +#endif if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1057,7 +985,7 @@ namespace jwt { private: /// OpenSSL structure containing converted keys - helper::evp_pkey_handle pkey; + std::shared_ptr pkey; /// Hash generator const EVP_MD* (*md)(); /// algorithm's name @@ -1088,13 +1016,13 @@ namespace jwt { pkey = helper::load_public_ec_key_from_string(public_key, public_key_password); check_public_key(pkey.get()); } else { - throw error::ecdsa_exception(error::ecdsa_error::no_key_provided); + throw ecdsa_exception(error::ecdsa_error::no_key_provided); } - if (!pkey) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + if (!pkey) throw ecdsa_exception(error::ecdsa_error::invalid_key); size_t keysize = EVP_PKEY_bits(pkey.get()); if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) - throw error::ecdsa_exception(error::ecdsa_error::invalid_key_size); + throw ecdsa_exception(error::ecdsa_error::invalid_key_size); } /** @@ -1105,7 +1033,11 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - auto ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); +#endif if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1145,7 +1077,11 @@ namespace jwt { std::string der_signature = p1363_to_der_signature(signature, ec); if (ec) { return; } - auto ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); +#endif if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1186,14 +1122,12 @@ namespace jwt { #ifdef JWT_OPENSSL_3_0 std::unique_ptr ctx( EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); - if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } - if (EVP_PKEY_public_check(ctx.get()) != 1) { - throw error::ecdsa_exception(error::ecdsa_error::invalid_key); - } + if (!ctx) { throw ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_public_check(ctx.get()) != 1) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } #else std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); - if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } - if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + if (!eckey) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); #endif } @@ -1201,22 +1135,19 @@ namespace jwt { #ifdef JWT_OPENSSL_3_0 std::unique_ptr ctx( EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); - if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } - if (EVP_PKEY_private_check(ctx.get()) != 1) { - throw error::ecdsa_exception(error::ecdsa_error::invalid_key); - } + if (!ctx) { throw ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_private_check(ctx.get()) != 1) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } #else std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); - if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } - if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + if (!eckey) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); #endif } std::string der_to_p1363_signature(const std::string& der_signature, std::error_code& ec) const { const unsigned char* possl_signature = reinterpret_cast(der_signature.data()); std::unique_ptr sig( - d2i_ECDSA_SIG(nullptr, &possl_signature, static_cast(der_signature.length())), - ECDSA_SIG_free); + d2i_ECDSA_SIG(nullptr, &possl_signature, der_signature.length()), ECDSA_SIG_free); if (!sig) { ec = error::signature_generation_error::signature_decoding_failed; return {}; @@ -1278,7 +1209,7 @@ namespace jwt { } /// OpenSSL struct containing keys - helper::evp_pkey_handle pkey; + std::shared_ptr pkey; /// Hash generator function const EVP_MD* (*md)(); /// algorithm's name @@ -1315,7 +1246,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw error::ecdsa_exception(error::ecdsa_error::load_key_bio_read); + throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); } /** * Sign jwt data @@ -1325,7 +1256,12 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - auto ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr ctx(EVP_MD_CTX_create(), + &EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); +#endif if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1373,7 +1309,12 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); - auto ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr ctx(EVP_MD_CTX_create(), + &EVP_MD_CTX_destroy); +#else + std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); +#endif if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1414,7 +1355,7 @@ namespace jwt { private: /// OpenSSL struct containing keys - helper::evp_pkey_handle pkey; + std::shared_ptr pkey; /// algorithm's name const std::string alg_name; }; @@ -1440,7 +1381,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw error::rsa_exception(error::rsa_error::no_key_provided); + throw rsa_exception(error::rsa_error::no_key_provided); } /** @@ -1451,7 +1392,12 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); - auto md_ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr md_ctx(EVP_MD_CTX_create(), + &EVP_MD_CTX_destroy); +#else + std::unique_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); +#endif if (!md_ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1500,7 +1446,12 @@ namespace jwt { void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); - auto md_ctx = helper::make_evp_md_ctx(); +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr md_ctx(EVP_MD_CTX_create(), + &EVP_MD_CTX_destroy); +#else + std::unique_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); +#endif if (!md_ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1540,7 +1491,7 @@ namespace jwt { private: /// OpenSSL structure containing keys - helper::evp_pkey_handle pkey; + std::shared_ptr pkey; /// Hash generator function const EVP_MD* (*md)(); /// algorithm's name @@ -1813,6 +1764,9 @@ namespace jwt { #ifdef __cpp_lib_experimental_detect template class _Op, typename... _Args> using is_detected = std::experimental::is_detected<_Op, _Args...>; + + template class _Op, typename... _Args> + using is_detected_t = std::experimental::detected_t<_Op, _Args...>; #else struct nonesuch { nonesuch() = delete; @@ -1838,58 +1792,139 @@ namespace jwt { template class Op, class... Args> using is_detected = typename detector::value; + + template class Op, class... Args> + using is_detected_t = typename detector::type; #endif - template - using is_signature = typename std::is_same; + template + using get_type_function = decltype(traits_type::get_type); - template class Op, typename Signature> - struct is_function_signature_detected { - using type = Op; - static constexpr auto value = is_detected::value && std::is_function::value && - is_signature::value; - }; + template + using is_get_type_signature = + typename std::is_same, json::type(const value_type&)>; template struct supports_get_type { - template - using get_type_t = decltype(T::get_type); + static constexpr auto value = is_detected::value && + std::is_function>::value && + is_get_type_signature::value; + }; - static constexpr auto value = - is_function_signature_detected::value; + template + using as_object_function = decltype(traits_type::as_object); - // Internal assertions for better feedback - static_assert(value, "traits implementation must provide `jwt::json::type get_type(const value_type&)`"); - }; - -#define JWT_CPP_JSON_TYPE_TYPE(TYPE) json_##TYPE_type -#define JWT_CPP_AS_TYPE_T(TYPE) as_##TYPE_t -#define JWT_CPP_SUPPORTS_AS(TYPE) \ - template \ - struct supports_as_##TYPE { \ - template \ - using JWT_CPP_AS_TYPE_T(TYPE) = decltype(T::as_##TYPE); \ - \ - static constexpr auto value = \ - is_function_signature_detected::value; \ - \ - static_assert(value, "traits implementation must provide `" #TYPE "_type as_" #TYPE "(const value_type&)`"); \ - } + template + using is_as_object_signature = + typename std::is_same, object_type(const value_type&)>; + + template + struct supports_as_object { + static constexpr auto value = std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_object_signature::value; + }; + + template + using as_array_function = decltype(traits_type::as_array); + + template + using is_as_array_signature = + typename std::is_same, array_type(const value_type&)>; + + template + struct supports_as_array { + static constexpr auto value = std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_array_signature::value; + }; + + template + using as_string_function = decltype(traits_type::as_string); + + template + using is_as_string_signature = + typename std::is_same, string_type(const value_type&)>; + + template + struct supports_as_string { + static constexpr auto value = std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_string_signature::value; + }; + + template + using as_number_function = decltype(traits_type::as_number); + + template + using is_as_number_signature = + typename std::is_same, number_type(const value_type&)>; + + template + struct supports_as_number { + static constexpr auto value = std::is_floating_point::value && + std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_number_signature::value; + }; - JWT_CPP_SUPPORTS_AS(object); - JWT_CPP_SUPPORTS_AS(array); - JWT_CPP_SUPPORTS_AS(string); - JWT_CPP_SUPPORTS_AS(number); - JWT_CPP_SUPPORTS_AS(integer); - JWT_CPP_SUPPORTS_AS(boolean); + template + using as_integer_function = decltype(traits_type::as_int); -#undef JWT_CPP_JSON_TYPE_TYPE -#undef JWT_CPP_AS_TYPE_T -#undef JWT_CPP_SUPPORTS_AS + template + using is_as_integer_signature = + typename std::is_same, integer_type(const value_type&)>; + + template + struct supports_as_integer { + static constexpr auto value = std::is_signed::value && + !std::is_floating_point::value && + std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_integer_signature::value; + }; + + template + using as_boolean_function = decltype(traits_type::as_bool); + + template + using is_as_boolean_signature = + typename std::is_same, boolean_type(const value_type&)>; + + template + struct supports_as_boolean { + static constexpr auto value = std::is_convertible::value && + std::is_constructible::value && + is_detected::value && + std::is_function>::value && + is_as_boolean_signature::value; + }; template struct is_valid_traits { + // Internal assertions for better feedback + static_assert(supports_get_type::value, + "traits must provide `jwt::json::type get_type(const value_type&)`"); + static_assert(supports_as_object::value, + "traits must provide `object_type as_object(const value_type&)`"); + static_assert(supports_as_array::value, + "traits must provide `array_type as_array(const value_type&)`"); + static_assert(supports_as_string::value, + "traits must provide `string_type as_string(const value_type&)`"); + static_assert(supports_as_number::value, + "traits must provide `number_type as_number(const value_type&)`"); + static_assert( + supports_as_integer::value, + "traits must provide `integer_type as_int(const value_type&)`"); + static_assert( + supports_as_boolean::value, + "traits must provide `boolean_type as_bool(const value_type&)`"); + static constexpr auto value = supports_get_type::value && supports_as_object::value && @@ -1910,41 +1945,76 @@ namespace jwt { // TODO(prince-chrismc): Stream operators }; - // https://stackoverflow.com/a/53967057/8480874 - template - struct is_iterable : std::false_type {}; + template + using has_mapped_type = typename traits_type::mapped_type; - template - struct is_iterable())), decltype(std::end(std::declval())), -#if __cplusplus > 201402L - decltype(std::cbegin(std::declval())), decltype(std::cend(std::declval())) -#else - decltype(std::begin(std::declval())), - decltype(std::end(std::declval())) -#endif - >> : std::true_type { + template + using has_key_type = typename traits_type::key_type; + + template + using has_value_type = typename traits_type::value_type; + + template + using has_iterator = typename object_type::iterator; + + template + using has_const_iterator = typename object_type::const_iterator; + + template + using is_begin_signature = + typename std::is_same().begin()), has_iterator>; + + template + using is_begin_const_signature = + typename std::is_same().begin()), has_const_iterator>; + + template + struct supports_begin { + static constexpr auto value = + is_detected::value && is_detected::value && + is_begin_signature::value && is_begin_const_signature::value; }; -#if __cplusplus > 201703L - template - inline constexpr bool is_iterable_v = is_iterable::value; -#endif + template + using is_end_signature = + typename std::is_same().end()), has_iterator>; - template - using is_count_signature = typename std::is_integral().count( - std::declval()))>; + template + using is_end_const_signature = + typename std::is_same().end()), has_const_iterator>; - template - struct is_subcription_operator_signature : std::false_type {}; + template + struct supports_end { + static constexpr auto value = + is_detected::value && is_detected::value && + is_end_signature::value && is_end_const_signature::value; + }; template - struct is_subcription_operator_signature< - object_type, string_type, - void_t().operator[](std::declval()))>> : std::true_type { - // TODO(prince-chrismc): I am not convienced this is meaningful anymore - static_assert( - value, - "object_type must implementate the subscription operator '[]' taking string_type as an arguement"); + using is_count_signature = typename std::is_integral().count(std::declval()))>; + + template + struct has_subcription_operator { + template + struct sfinae_true : std::true_type {}; + + template + static auto test_operator_plus(int) + -> sfinae_true().operator[](std::declval()))>; + template + static auto test_operator_plus(long) -> std::false_type; + + static constexpr auto value = decltype(test_operator_plus(0)){}; + }; + + template + struct is_subcription_operator_signature { + static constexpr auto has_subscription_operator = has_subcription_operator::value; + static_assert(has_subscription_operator, + "object_type must implementate the subscription operator '[]' for this library"); + + static constexpr auto value = has_subscription_operator; }; template @@ -1954,37 +2024,25 @@ namespace jwt { template struct is_valid_json_object { - template - using mapped_type_t = typename T::mapped_type; - template - using key_type_t = typename T::key_type; - template - using iterator_t = typename T::iterator; - template - using const_iterator_t = typename T::const_iterator; - static constexpr auto value = - std::is_constructible::value && - is_detected::value && + is_detected::value && std::is_same::value && - is_detected::value && + is_detected::value && (std::is_same::value || std::is_constructible::value) && - is_detected::value && is_detected::value && - is_iterable::value && is_count_signature::value && - is_subcription_operator_signature::value && + supports_begin::value && supports_end::value && + is_count_signature::value && + is_subcription_operator_signature::value && is_at_const_signature::value; + + static constexpr auto supports_claims_transform = + value && is_detected::value && + std::is_same>::value; }; template struct is_valid_json_array { - template - using value_type_t = typename T::value_type; - - static constexpr auto value = std::is_constructible::value && - is_iterable::value && - is_detected::value && - std::is_same::value; + static constexpr auto value = std::is_same::value; }; template @@ -1998,46 +2056,42 @@ namespace jwt { typename std::is_same().substr(std::declval())), string_type>; + template + struct has_operate_plus_method { // https://stackoverflow.com/a/9154394/8480874 + template + struct sfinae_true : std::true_type {}; + + template + static auto test_operator_plus(int) + -> sfinae_true().operator+(std::declval()))>; + template + static auto test_operator_plus(long) -> std::false_type; + + static constexpr auto value = decltype(test_operator_plus(0)){}; + }; + template using is_std_operate_plus_signature = typename std::is_same(), std::declval())), string_type>; - template + template struct is_valid_json_string { static constexpr auto substr = is_substr_start_end_index_signature::value && is_substr_start_index_signature::value; static_assert(substr, "string_type must have a substr method taking only a start index and an overload " "taking a start and end index, both must return a string_type"); - static constexpr auto operator_plus = is_std_operate_plus_signature::value; + static constexpr auto operator_plus = + has_operate_plus_method::value || is_std_operate_plus_signature::value; static_assert(operator_plus, "string_type must have a '+' operator implemented which returns the concatenated string"); - static constexpr auto value = - std::is_constructible::value && substr && operator_plus; - }; - - template - struct is_valid_json_number { - static constexpr auto value = - std::is_floating_point::value && std::is_constructible::value; - }; - - template - struct is_valid_json_integer { - static constexpr auto value = std::is_signed::value && - !std::is_floating_point::value && - std::is_constructible::value; - }; - template - struct is_valid_json_boolean { - static constexpr auto value = std::is_convertible::value && - std::is_constructible::value; + static constexpr auto value = substr && operator_plus; }; - template + template struct is_valid_json_types { // Internal assertions for better feedback static_assert(is_valid_json_value::value, @@ -2047,13 +2101,10 @@ namespace jwt { static_assert(is_valid_json_array::value, "array_type must be a container of value_type"); - static constexpr auto value = is_valid_json_value::value && - is_valid_json_object::value && + static constexpr auto value = is_valid_json_object::value && + is_valid_json_value::value && is_valid_json_array::value && - is_valid_json_string::value && - is_valid_json_number::value && - is_valid_json_integer::value && - is_valid_json_boolean::value; + is_valid_json_string::value; }; } // namespace details @@ -2078,10 +2129,9 @@ namespace jwt { "string_type must be a std::string, convertible to a std::string, or construct a std::string."); static_assert( - details::is_valid_json_types::value, + details::is_valid_json_types::value, "must staisfy json container requirements"); static_assert(details::is_valid_traits::value, "traits must satisfy requirements"); @@ -2139,18 +2189,11 @@ namespace jwt { typename json_traits::string_type as_string() const { return json_traits::as_string(val); } /** - * \brief Get the contained JSON value as a date - * - * If the value is a decimal, it is rounded up to the closest integer - * + * Get the contained JSON value as a date * \return content as date * \throw std::bad_cast Content was not a date */ - date as_date() const { - using std::chrono::system_clock; - if (get_type() == json::type::number) return system_clock::from_time_t(std::round(as_number())); - return system_clock::from_time_t(as_integer()); - } + date as_date() const { return std::chrono::system_clock::from_time_t(as_int()); } /** * Get the contained JSON value as an array @@ -2177,14 +2220,14 @@ namespace jwt { * \return content as int * \throw std::bad_cast Content was not an int */ - typename json_traits::integer_type as_integer() const { return json_traits::as_integer(val); } + typename json_traits::integer_type as_int() const { return json_traits::as_int(val); } /** * Get the contained JSON value as a bool * \return content as bool * \throw std::bad_cast Content was not a bool */ - typename json_traits::boolean_type as_boolean() const { return json_traits::as_boolean(val); } + typename json_traits::boolean_type as_bool() const { return json_traits::as_bool(val); } /** * Get the contained JSON value as a number @@ -2211,8 +2254,10 @@ namespace jwt { namespace details { template - struct map_of_claims { + class map_of_claims { typename json_traits::object_type claims; + + public: using basic_claim_t = basic_claim; using iterator = typename json_traits::object_type::iterator; using const_iterator = typename json_traits::object_type::const_iterator; @@ -2266,6 +2311,21 @@ namespace jwt { if (!has_claim(name)) throw error::claim_not_present_exception(); return basic_claim_t{claims.at(name)}; } + + std::unordered_map get_claims() const { + static_assert( + details::is_valid_json_object::supports_claims_transform, + "currently there is a limitation on the internal implemantation of the `object_type` to have an " + "`std::pair` like `value_type`"); + + std::unordered_map res; + std::transform(claims.begin(), claims.end(), std::inserter(res, res.end()), + [](const typename json_traits::object_type::value_type& val) { + return std::make_pair(val.first, basic_claim_t{val.second}); + }); + return res; + } }; } // namespace details @@ -2470,7 +2530,7 @@ namespace jwt { class decoded_jwt : public header, public payload { protected: /// Unmodifed token, as passed to constructor - typename json_traits::string_type token; + const typename json_traits::string_type token; /// Header part decoded from base64 typename json_traits::string_type header; /// Unmodified header part in base64 @@ -2566,15 +2626,19 @@ namespace jwt { */ const typename json_traits::string_type& get_signature_base64() const noexcept { return signature_base64; } /** - * Get all payload as JSON object + * Get all payload claims * \return map of claims */ - typename json_traits::object_type get_payload_json() const { return this->payload_claims.claims; } + std::unordered_map get_payload_claims() const { + return this->payload_claims.get_claims(); + } /** - * Get all header as JSON object + * Get all header claims * \return map of claims */ - typename json_traits::object_type get_header_json() const { return this->header_claims.claims; } + std::unordered_map get_header_claims() const { + return this->header_claims.get_claims(); + } /** * Get a payload claim by name * @@ -2888,8 +2952,8 @@ namespace jwt { if (ec) return; const bool matches = [&]() { switch (expected.get_type()) { - case json::type::boolean: return expected.as_boolean() == jc.as_boolean(); - case json::type::integer: return expected.as_integer() == jc.as_integer(); + case json::type::boolean: return expected.as_bool() == jc.as_bool(); + case json::type::integer: return expected.as_int() == jc.as_int(); case json::type::number: return expected.as_number() == jc.as_number(); case json::type::string: return expected.as_string() == jc.as_string(); case json::type::array: @@ -2989,18 +3053,11 @@ namespace jwt { } static std::string to_lower_unicode(const std::string& str, const std::locale& loc) { -#if __cplusplus > 201103L std::wstring_convert, wchar_t> conv; auto wide = conv.from_bytes(str); auto& f = std::use_facet>(loc); f.tolower(&wide[0], &wide[0] + wide.size()); return conv.to_bytes(wide); -#else - std::string result; - std::transform(str.begin(), str.end(), std::back_inserter(result), - [&loc](unsigned char c) { return std::tolower(c, loc); }); - return result; -#endif } }; } // namespace verify_ops @@ -3444,12 +3501,6 @@ namespace jwt { } bool empty() const noexcept { return jwk_claims.empty(); } - - /** - * Get all jwk claims - * \return Map of claims - */ - typename json_traits::object_type get_claims() const { return this->jwk_claims.claims; } }; /** diff --git a/README.md b/README.md index 050394924..b4e043dcf 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA IS-07 NMOS Event & Tally Specification](https://specs.amwa.tv/is-07/) - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) - [AMWA IS-09 NMOS System Parameters Specification](https://specs.amwa.tv/is-09/) (originally defined in JT-NM TR-1001-1:2018 Annex A) +- [AMWA IS-10 NMOS Authorization Specification](https://specs.amwa.tv/is-10/) - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) - [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) @@ -112,6 +113,7 @@ The implementation is designed to be extended. Development is ongoing, following Recent activity on the project (newest first): +- Added support for IS-10 Autorization API - Added support for HSTS and OCSP stapling - Added support for BCP-006-01 v1.0-dev, which can be demonstrated with **nmos-cpp-node** by using `"video_type": "video/jxsv"` - Updates to the GitHub Actions build-test workflow for better coverage of platforms and to include unicast DNS-SD tests From 3ed177c9cbb4dda4c9f56447cc38fe8e29198e0b Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 10 Oct 2023 23:32:47 +0100 Subject: [PATCH 081/121] Add missing files for jwt-cpp 0.6.0 --- .../jwt-cpp/traits/nlohmann-json/defaults.h | 82 +++++++++++++++++++ .../jwt-cpp/traits/nlohmann-json/traits.h | 77 +++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h create mode 100644 Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h diff --git a/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h b/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h new file mode 100644 index 000000000..10b9a5af2 --- /dev/null +++ b/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h @@ -0,0 +1,82 @@ +#ifndef JWT_CPP_NLOHMANN_JSON_DEFAULTS_H +#define JWT_CPP_NLOHMANN_JSON_DEFAULTS_H + +#ifndef JWT_DISABLE_PICOJSON +#define JWT_DISABLE_PICOJSON +#endif + +#include "traits.h" + +namespace jwt { + /** + * \brief a class to store a generic [JSON for Modern C++](https://github.com/nlohmann/json) value as claim + * + * This type is the specialization of the \ref basic_claim class which + * uses the standard template types. + */ + using claim = basic_claim; + + /** + * Create a verifier using the default clock + * \return verifier instance + */ + inline verifier verify() { + return verify(default_clock{}); + } + + /** + * Return a builder instance to create a new token + */ + inline builder create() { return builder(); } + +#ifndef JWT_DISABLE_BASE64 + /** + * Decode a token + * \param token Token to decode + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + inline decoded_jwt decode(const std::string& token) { + return decoded_jwt(token); + } +#endif + + /** + * Decode a token + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64url decode and + * return the results. + * \param token Token to decode + * \param decode The token to parse + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const std::string& token, Decode decode) { + return decoded_jwt(token, decode); + } + + /** + * Parse a jwk + * \param token JWK Token to parse + * \return Parsed JWK + * \throw std::runtime_error Token is not in correct format + */ + inline jwk parse_jwk(const traits::nlohmann_json::string_type& token) { + return jwk(token); + } + + /** + * Parse a jwks + * \param token JWKs Token to parse + * \return Parsed JWKs + * \throw std::runtime_error Token is not in correct format + */ + inline jwks parse_jwks(const traits::nlohmann_json::string_type& token) { + return jwks(token); + } +} // namespace jwt + +#endif // JWT_CPP_NLOHMANN_JSON_DEFAULTS_H diff --git a/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h b/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h new file mode 100644 index 000000000..23c2d7d10 --- /dev/null +++ b/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h @@ -0,0 +1,77 @@ +#ifndef JWT_CPP_NLOHMANN_JSON_TRAITS_H +#define JWT_CPP_NLOHMANN_JSON_TRAITS_H + +#include "jwt-cpp/jwt.h" +#include "nlohmann/json.hpp" + +namespace jwt { + namespace traits { + struct nlohmann_json { + using json = nlohmann::json; + using value_type = json; + using object_type = json::object_t; + using array_type = json::array_t; + using string_type = std::string; // current limitation of traits implementation + using number_type = json::number_float_t; + using integer_type = json::number_integer_t; + using boolean_type = json::boolean_t; + + static jwt::json::type get_type(const json& val) { + using jwt::json::type; + + if (val.type() == json::value_t::boolean) return type::boolean; + // nlohmann internally tracks two types of integers + if (val.type() == json::value_t::number_integer) return type::integer; + if (val.type() == json::value_t::number_unsigned) return type::integer; + if (val.type() == json::value_t::number_float) return type::number; + if (val.type() == json::value_t::string) return type::string; + if (val.type() == json::value_t::array) return type::array; + if (val.type() == json::value_t::object) return type::object; + + throw std::logic_error("invalid type"); + } + + static json::object_t as_object(const json& val) { + if (val.type() != json::value_t::object) throw std::bad_cast(); + return val.get(); + } + + static std::string as_string(const json& val) { + if (val.type() != json::value_t::string) throw std::bad_cast(); + return val.get(); + } + + static json::array_t as_array(const json& val) { + if (val.type() != json::value_t::array) throw std::bad_cast(); + return val.get(); + } + + static int64_t as_int(const json& val) { + switch (val.type()) { + case json::value_t::number_integer: + case json::value_t::number_unsigned: return val.get(); + default: throw std::bad_cast(); + } + } + + static bool as_bool(const json& val) { + if (val.type() != json::value_t::boolean) throw std::bad_cast(); + return val.get(); + } + + static double as_number(const json& val) { + if (val.type() != json::value_t::number_float) throw std::bad_cast(); + return val.get(); + } + + static bool parse(json& val, std::string str) { + val = json::parse(str.begin(), str.end()); + return true; + } + + static std::string serialize(const json& val) { return val.dump(); } + }; + } // namespace traits +} // namespace jwt + +#endif From 62c057099dcff54c5cb806e229db3af8b373bc18 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Tue, 10 Oct 2023 23:50:23 +0100 Subject: [PATCH 082/121] Code tidy up --- Development/nmos/jwt_generator_impl.cpp | 2 +- Development/nmos/jwt_validator.h | 2 +- Development/nmos/jwt_validator_impl.cpp | 384 ++++++++++++------------ 3 files changed, 196 insertions(+), 192 deletions(-) diff --git a/Development/nmos/jwt_generator_impl.cpp b/Development/nmos/jwt_generator_impl.cpp index ee6730214..b03b3e486 100644 --- a/Development/nmos/jwt_generator_impl.cpp +++ b/Development/nmos/jwt_generator_impl.cpp @@ -1,7 +1,7 @@ #include "nmos/jwt_generator.h" -#include #include "cpprest/basic_utils.h" +#include "jwt-cpp/traits/nlohmann-json/traits.h" #include "nmos/id.h" #include "nmos/jwk_utils.h" diff --git a/Development/nmos/jwt_validator.h b/Development/nmos/jwt_validator.h index 958be2871..331bcc626 100644 --- a/Development/nmos/jwt_validator.h +++ b/Development/nmos/jwt_validator.h @@ -39,7 +39,7 @@ namespace nmos class jwt_validator_impl; } - // callback for validating bearer_token + // callback for JSON validating access token typedef std::function token_validator; class jwt_validator diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index e788de200..7db1c1148 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -1,12 +1,12 @@ #include "nmos/jwt_validator.h" #include -#include #include "cpprest/basic_utils.h" #include "cpprest/http_msg.h" #include "cpprest/json.h" #include "cpprest/regex_utils.h" #include "cpprest/uri_schemes.h" +#include "jwt-cpp/traits/nlohmann-json/traits.h" #include "nmos/authorization_utils.h" #include "nmos/json_fields.h" @@ -107,7 +107,7 @@ namespace nmos // verify JWT is well formed auto decoded_token = jwt::decode(utility::us2s(token)); - // validate bearer token payload JSON + // do bearer token payload JSON validation if (token_validation) { token_validation(web::json::value::parse(utility::s2us(decoded_token.get_payload()))); @@ -115,250 +115,254 @@ namespace nmos std::vector errors; - if (validators.size()) + if (0 == validators.size()) { - const auto validate_scope = !scope.name.empty(); + // no JWT validator to perform access token validation + errors.push_back("no JWT validator to perform access token validation"); + } + + const auto validate_scope = !scope.name.empty(); - for (const auto& validator : validators) + for (const auto& validator : validators) + { + try { - try - { - // verify the signature & some common claims, such as exp, iat, nbf etc - validator.verify(decoded_token); + // verify the signature & some common claims, such as exp, iat, nbf etc + validator.verify(decoded_token); - // common claims verified (i.e. validator/public key successfully verify the token's signature), - // from this point onwards any error detected will be treated as failure + // common claims verified (i.e. validator/public key successfully verify the token's signature), + // from this point onwards any error detected will be treated as failure - // verify Registered Claims + // verify Registered Claims - // iss (Identifies principal that issued the JWT) - // The "iss" value is a case-sensitive string containing a StringOrURI value. - // see https://tools.ietf.org/html/rfc7519#section-4.1.1 - // iss is not needed to validate as this token may be coming from an alternative Authorization server, which would have a different iss then the current in used Authorization server. + // iss (Identifies principal that issued the JWT) + // The "iss" value is a case-sensitive string containing a StringOrURI value. + // see https://tools.ietf.org/html/rfc7519#section-4.1.1 + // iss is not needed to validate as this token may be coming from an alternative Authorization server, which would have a different iss then the current in used Authorization server. - // sub (Identifies the subject of the JWT) - // hmm, not sure how to verify sub as it could be anything - // see https://tools.ietf.org/html/rfc7519#section-4.1.2 + // sub (Identifies the subject of the JWT) + // hmm, not sure how to verify sub as it could be anything + // see https://tools.ietf.org/html/rfc7519#section-4.1.2 - // aud (Identifies the recipients of the JWT) - // This claim MUST be a JSON array containing the fully resolved domain names of the intended recipients, or a domain name containing - // wild - card characters in order to target a subset of devices on a network. Such wild-carding of domain names is documented in RFC 4592. - // If aud claim does not match the fully resolved domain name of the resource server, the Resource Server MUST reject the token. - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#aud - // see https://tools.ietf.org/html/rfc7519#section-4.1.3 + // aud (Identifies the recipients of the JWT) + // This claim MUST be a JSON array containing the fully resolved domain names of the intended recipients, or a domain name containing + // wild - card characters in order to target a subset of devices on a network. Such wild-carding of domain names is documented in RFC 4592. + // If aud claim does not match the fully resolved domain name of the resource server, the Resource Server MUST reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#aud + // see https://tools.ietf.org/html/rfc7519#section-4.1.3 - auto verify_aud = [&decoded_token](const utility::string_t& audience_) + auto verify_aud = [&decoded_token](const utility::string_t& audience_) + { + auto strip_trailing_dot = [](const std::string& audience_) { - auto strip_trailing_dot = [](const std::string& audience_) { - auto audience = audience_; - if (!audience.empty() && U('.') == audience.back()) - { - audience.pop_back(); - } - return audience; - }; + auto audience = audience_; + if (!audience.empty() && U('.') == audience.back()) + { + audience.pop_back(); + } + return audience; + }; + + auto audience = strip_trailing_dot(utility::us2s(audience_)); + std::vector segments; + boost::split(segments, audience, boost::is_any_of(".")); + + const auto& auds = decoded_token.get_audience(); + for (const auto& aud_ : auds) + { + // strip the scheme (https://) if presented + auto aud = strip_trailing_dot(aud_); + web::http::uri aud_uri(utility::s2us(aud)); + if (!aud_uri.scheme().empty()) + { + aud = utility::us2s(aud_uri.host()); + } + + // is the audience an exact match to the token audience + if (audience == aud) + { + return true; + } - auto audience = strip_trailing_dot(utility::us2s(audience_)); - std::vector segments; - boost::split(segments, audience, boost::is_any_of(".")); + // do reverse segment matching between audience and token audience + std::vector aud_segments; + boost::split(aud_segments, aud, boost::is_any_of(".")); - const auto& auds = decoded_token.get_audience(); - for (const auto& aud_ : auds) + if (segments.size() >= aud_segments.size() && aud_segments.size()) { - // strip the scheme (https://) if presented - auto aud = strip_trailing_dot(aud_); - web::http::uri aud_uri(utility::s2us(aud)); - if (!aud_uri.scheme().empty()) + // token audience got to be in wildcard domain name format, leftmost is a "*" charcater + // if not it is not going to match + // see https://tools.ietf.org/html/rfc4592#section-2.1.1 + if (aud_segments[0] != "*") { - aud = utility::us2s(aud_uri.host()); + return false; } - // is the audience an exact match to the token audience - if (audience == aud) + // token audience is in wildcard domain name format + // let's do a segment to segment comparison between audience and token audience + bool matched{ true }; + auto idx = aud_segments.size() - 1; + for (auto it = aud_segments.rbegin(); it != aud_segments.rend() && matched; ++it) + { + if (idx && *it != segments[idx--]) + { + matched = false; + } + } + if (matched) { return true; } + } + } + return false; + }; + if (!verify_aud(audience)) + { + throw insufficient_scope_exception(utility::us2s(audience) + " not found in audience"); + } - // do reverse segment matching between audience and token audience - std::vector aud_segments; - boost::split(aud_segments, aud, boost::is_any_of(".")); + // scope optional + // If scope claim does not contain the expected scope, the Resource Server reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope + auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) + { + if (decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + { + const auto& scope_claim = decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto scopes_set = scopes(utility::s2us(scope_claim.as_string())); + return (scopes_set.end() != std::find(scopes_set.begin(), scopes_set.end(), scope)); + } + return true; + }; + if (validate_scope && !verify_scope(scope)) + { + throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); + } - if (segments.size() >= aud_segments.size() && aud_segments.size()) + // verify Private Claims + + // x-nmos-* (Contains information particular to the NMOS API the token is intended for) + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + auto verify_x_nmos_scope_claim = [&decoded_token, req](const std::string& x_nmos_scope_claim_, const std::string& path) + { + if (!decoded_token.has_payload_claim(x_nmos_scope_claim_)) { return false; } + const auto x_nmos_scope_claim = decoded_token.get_payload_claim(x_nmos_scope_claim_).to_json(); + + if (!x_nmos_scope_claim.is_null()) + { + auto accessible = [&x_nmos_scope_claim, req, &path](const std::string& access_right) + { + if (x_nmos_scope_claim.contains(access_right)) { - // token audience got to be in wildcard domain name format, leftmost is a "*" charcater - // if not it is not going to match - // see https://tools.ietf.org/html/rfc4592#section-2.1.1 - if (aud_segments[0] != "*") + auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); + for (auto& accessible_path : accessible_paths) { - return false; - } + // construct path regex for regex comparison - // token audience is in wildcard domain name format - // let's do a segment to segment comparison between audience and token audience - bool matched{ true }; - auto idx = aud_segments.size() - 1; - for (auto it = aud_segments.rbegin(); it != aud_segments.rend() && matched; ++it) - { - if (idx && *it != segments[idx--]) + auto acc_path = accessible_path.get(); + // replace any '*' => '.*' + boost::replace_all(acc_path, "*", ".*"); + const bst::regex path_regex(acc_path); + if (bst::regex_match(path, path_regex)) { - matched = false; + return true; } } - if (matched) - { - return true; - } } + return false; + }; + + // write accessible + if ((web::http::methods::POST == req.method()) + || (web::http::methods::PUT == req.method()) + || (web::http::methods::PATCH == req.method()) + || (web::http::methods::DEL == req.method())) + { + return accessible("write"); } - return false; - }; - if (!verify_aud(audience)) - { - throw insufficient_scope_exception(utility::us2s(audience) + " not found in audience"); - } - // scope optional - // If scope claim does not contain the expected scope, the Resource Server reject the token. - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope - auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) - { - if (decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + // read accessible + if ((web::http::methods::OPTIONS == req.method()) + || (web::http::methods::GET == req.method()) + || (web::http::methods::HEAD == req.method())) { - const auto& scope_claim = decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::scope)); - const auto scopes_set = scopes(utility::s2us(scope_claim.as_string())); - return (scopes_set.end() != std::find(scopes_set.begin(), scopes_set.end(), scope)); + return accessible("read"); } - return true; - }; - if (validate_scope && !verify_scope(scope)) - { - throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); } + return false; + }; - // verify Private Claims + // verify the relevant x-nmos-* private claim + if (validate_scope) + { + const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); - // x-nmos-* (Contains information particular to the NMOS API the token is intended for) - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- - auto verify_x_nmos_scope_claim = [&decoded_token, req](const std::string& x_nmos_scope_claim_, const std::string& path) + // extract from /x-nmos/// + auto extract_path = [req](const nmos::experimental::scope& scope) { - if (!decoded_token.has_payload_claim(x_nmos_scope_claim_)) { return false; } - const auto x_nmos_scope_claim = decoded_token.get_payload_claim(x_nmos_scope_claim_).to_json(); + const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); + const auto request_uri = utility::us2s(req.request_uri().to_string()); - if (!x_nmos_scope_claim.is_null()) + if (bst::regex_search(request_uri, search_regex)) { - auto accessible = [&x_nmos_scope_claim, req, &path](const std::string& access_right) + auto path = bst::regex_replace(request_uri, search_regex, ""); + if (path.size() && ('/' == path[0])) { - if (x_nmos_scope_claim.contains(access_right)) - { - auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); - for (auto& accessible_path : accessible_paths) - { - // construct path regex for regex comparison - - auto acc_path = accessible_path.get(); - // replace any '*' => '.*' - boost::replace_all(acc_path, "*", ".*"); - const bst::regex path_regex(acc_path); - if (bst::regex_match(path, path_regex)) - { - return true; - } - } - } - return false; - }; - - // write accessible - if ((web::http::methods::POST == req.method()) - || (web::http::methods::PUT == req.method()) - || (web::http::methods::PATCH == req.method()) - || (web::http::methods::DEL == req.method())) - { - return accessible("write"); + return path.erase(0, 1); } - - // read accessible - if ((web::http::methods::OPTIONS == req.method()) - || (web::http::methods::GET == req.method()) - || (web::http::methods::HEAD == req.method())) + else { - return accessible("read"); + return std::string{}; } } - return false; + return std::string{};; }; + const auto path = extract_path(scope); - // verify the relevant x-nmos-* private claim - if (validate_scope) + if (path.empty()) { - const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); - - // extract from /x-nmos/// - auto extract_path = [req](const nmos::experimental::scope& scope) - { - const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); - const auto request_uri = utility::us2s(req.request_uri().to_string()); - - if (bst::regex_search(request_uri, search_regex)) - { - auto path = bst::regex_replace(request_uri, search_regex, ""); - if (path.size() && ('/' == path[0])) - { - return path.erase(0, 1); - } - else - { - return std::string{}; - } - } - return std::string{};; - }; - const auto path = extract_path(scope); + // The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation - if (path.empty()) + // if scope claim is presented, it has already verified eariler + if (!decoded_token.has_payload_claim(x_nmos_scope_claim) && !decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) { - // The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission. - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation - - // if scope claim is presented, it has already verified eariler - if (!decoded_token.has_payload_claim(x_nmos_scope_claim) && !decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) - { - // missing both x-nmos private claim and scope claim - throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); - } + // missing both x-nmos private claim and scope claim + throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); } - else - { - // The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens. - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + } + else + { + // The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation - if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) - { - throw insufficient_scope_exception("claim x-nmos-" + utility::us2s(scope.name) + " " + utility::us2s(req.request_uri().to_string()) + " not accessible"); - } + if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) + { + throw insufficient_scope_exception("claim x-nmos-" + utility::us2s(scope.name) + " " + utility::us2s(req.request_uri().to_string()) + " not accessible"); } } - - // token validate successfully - return; - } - catch (const insufficient_scope_exception&) - { - throw; - } - catch (const jwt::error::token_verification_exception& e) - { - throw std::invalid_argument(e.what()); - } - catch (const jwt::error::signature_verification_exception& e) - { - // ignore, try next validator - errors.push_back(e.what()); } + + // token validate successfully + return; + } + catch (const insufficient_scope_exception&) + { + throw; + } + catch (const jwt::error::token_verification_exception& e) + { + throw std::invalid_argument(e.what()); + } + catch (const jwt::error::signature_verification_exception& e) + { + // ignore, try next validator + errors.push_back(e.what()); } } - // reaching here, there must be no matching public key for the token + // reaching here, there must be because no matching public key to validate the access token // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it From e49280da4b3d175c3edf63cb434ffdb229e2b7b5 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 11 Oct 2023 10:14:04 +0100 Subject: [PATCH 083/121] Bump up jwt-cpp to v0.7.0-rc.0 for non-conan build to support GCC-4.8 and 0.6.0 for conan build --- Development/third_party/jwt-cpp/README.md | 30 +- Development/third_party/jwt-cpp/base.h | 301 ++++--- Development/third_party/jwt-cpp/jwt.h | 747 ++++++++---------- .../jwt-cpp/traits/nlohmann-json/defaults.h | 6 + .../jwt-cpp/traits/nlohmann-json/traits.h | 4 +- 5 files changed, 554 insertions(+), 534 deletions(-) diff --git a/Development/third_party/jwt-cpp/README.md b/Development/third_party/jwt-cpp/README.md index 0f4f365d9..93fb1ce2b 100644 --- a/Development/third_party/jwt-cpp/README.md +++ b/Development/third_party/jwt-cpp/README.md @@ -1,7 +1,7 @@ logo [![License Badge](https://img.shields.io/github/license/Thalhammer/jwt-cpp)](https://github.com/Thalhammer/jwt-cpp/blob/master/LICENSE) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/5f7055e294744901991fd0a1620b231d)](https://app.codacy.com/app/Thalhammer/jwt-cpp?utm_source=github.com&utm_medium=referral&utm_content=Thalhammer/jwt-cpp&utm_campaign=Badge_Grade_Settings) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/5f7055e294744901991fd0a1620b231d)](https://app.codacy.com/gh/Thalhammer/jwt-cpp/dashboard) [![Linux Badge][Linux]][Cross-Platform] [![MacOS Badge][MacOS]][Cross-Platform] [![Windows Badge][Windows]][Cross-Platform] @@ -40,22 +40,23 @@ In the name of flexibility and extensibility, jwt-cpp supports [OpenSSL](https:/ | OpenSSL | LibreSSL | wolfSSL | |-------------------|----------------|----------------| -| ![1.0.2u][o1.0.2] | ![3.2.7][l3.2] | ![5.0.0][w5.0] | -| ![1.1.0i][o1.1.0] | ![3.3.5][l3.3] | ![5.1.1][w5.1] | -| ![1.1.1m][o1.1.1] | ![3.4.2][l3.4] | | -| ![3.0.1][o3.0] | | | +| ![1.0.2u][o1.0.2] | ![3.3.6][l3.3] | ![5.1.1][w5.1] | +| ![1.1.0i][o1.1.0] | ![3.4.3][l3.4] | ![5.2.0][w5.2] | +| ![1.1.1q][o1.1.1] | ![3.5.3][l3.5] | ![5.3.0][w5.3] | +| ![3.0.5][o3.0] | | | > ℹ️ Note: A complete list of versions tested in the past can be found [here](https://github.com/Thalhammer/jwt-cpp/tree/badges). [o1.0.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.0.2u/shields.json [o1.1.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.1.0i/shields.json -[o1.1.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.1.1m/shields.json -[o3.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/3.0.1/shields.json -[l3.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.2.7/shields.json -[l3.3]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.3.5/shields.json -[l3.4]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.4.2/shields.json -[w5.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/wolfssl/5.0.0/shields.json +[o1.1.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/1.1.1q/shields.json +[o3.0]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/openssl/3.0.5/shields.json +[l3.3]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.3.6/shields.json +[l3.4]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.4.3/shields.json +[l3.5]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/libressl/3.5.3/shields.json [w5.1]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/wolfssl/5.1.1/shields.json +[w5.2]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/wolfssl/5.2.0/shields.json +[w5.3]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/wolfssl/5.3.0/shields.json ## Overview @@ -79,7 +80,7 @@ For your convience there are serval traits implementation which provide some pop [jsoncons]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/danielaparker-jsoncons/shields.json [boostjson]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/Thalhammer/jwt-cpp/badges/traits/boost-json/shields.json -In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. It's possible to directly include the traits defaults for the other JSON libraries. See the [traits examples](https://github.com/prince-chrismc/jwt-cpp/tree/master/example/traits) for details. +In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. It's possible to directly include the traits defaults for the other JSON libraries. See the [traits examples](https://github.com/Thalhammer/jwt-cpp/tree/master/example/traits) for details. As for the base64 requirements of JWTs, this libary provides `base.h` with all the required implentation; However base64 implementations are very common, with varying degrees of performance. When providing your own base64 implementation, you can define `JWT_DISABLE_BASE64` to remove the jwt-cpp implementation. @@ -95,7 +96,7 @@ int main() { std::string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; auto decoded = jwt::decode(token); - for(auto& e : decoded.get_payload_claims()) + for(auto& e : decoded.get_payload_json()) std::cout << e.first << " = " << e.second << std::endl; } ``` @@ -149,3 +150,6 @@ In order to build the test cases you also need ## Troubleshooting See the [FAQs](docs/faqs.md) for tips. + +## Conference Coverage +[![CppCon](https://img.youtube.com/vi/Oq4NW5idmiI/0.jpg)](https://www.youtube.com/watch?v=Oq4NW5idmiI) diff --git a/Development/third_party/jwt-cpp/base.h b/Development/third_party/jwt-cpp/base.h index 4177ed68d..cef493d19 100644 --- a/Development/third_party/jwt-cpp/base.h +++ b/Development/third_party/jwt-cpp/base.h @@ -1,9 +1,11 @@ #ifndef JWT_CPP_BASE_H #define JWT_CPP_BASE_H +#include #include #include #include +#include #ifdef __has_cpp_attribute #if __has_cpp_attribute(fallthrough) @@ -21,7 +23,10 @@ namespace jwt { */ namespace alphabet { /** - * \brief valid list of characted when working with [Base64](https://tools.ietf.org/html/rfc3548) + * \brief valid list of character when working with [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4) + * + * As directed in [X.509 Parameter](https://datatracker.ietf.org/doc/html/rfc7517#section-4.7) certificate chains are + * base64-encoded as per [Section 4 of RFC4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4) */ struct base64 { static const std::array& data() { @@ -38,7 +43,13 @@ namespace jwt { } }; /** - * \brief valid list of characted when working with [Base64URL](https://tools.ietf.org/html/rfc4648) + * \brief valid list of character when working with [Base64URL](https://tools.ietf.org/html/rfc4648#section-5) + * + * As directed by [RFC 7519 Terminology](https://datatracker.ietf.org/doc/html/rfc7519#section-2) set the definition of Base64URL + * encoding as that in [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-2) that states: + * + * > Base64 encoding using the URL- and filename-safe character set defined in + * > [Section 5 of RFC 4648 RFC4648](https://tools.ietf.org/html/rfc4648#section-5), with all trailing '=' characters omitted */ struct base64url { static const std::array& data() { @@ -54,155 +65,205 @@ namespace jwt { return fill; } }; + namespace helper { + /** + * @brief A General purpose base64url alphabet respecting the + * [URI Case Normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1) + * + * This is useful in situations outside of JWT encoding/decoding and is provided as a helper + */ + struct base64url_percent_encoding { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; + return data; + } + static const std::initializer_list& fill() { + static std::initializer_list fill{"%3D", "%3d"}; + return fill; + } + }; + } // namespace helper + + inline uint32_t index(const std::array& alphabet, char symbol) { + auto itr = std::find_if(alphabet.cbegin(), alphabet.cend(), [symbol](char c) { return c == symbol; }); + if (itr == alphabet.cend()) { throw std::runtime_error("Invalid input: not within alphabet"); } + + return std::distance(alphabet.cbegin(), itr); + } } // namespace alphabet /** - * \brief Alphabet generic methods for working with encoding/decoding the base64 family + * \brief A collection of fellable functions for working with base64 and base64url */ - class base { - public: - template - static std::string encode(const std::string& bin) { - return encode(bin, T::data(), T::fill()); - } - template - static std::string decode(const std::string& base) { - return decode(base, T::data(), T::fill()); - } - template - static std::string pad(const std::string& base) { - return pad(base, T::fill()); - } - template - static std::string trim(const std::string& base) { - return trim(base, T::fill()); - } + namespace base { - private: - static std::string encode(const std::string& bin, const std::array& alphabet, - const std::string& fill) { - size_t size = bin.size(); - std::string res; + namespace details { + struct padding { + size_t count = 0; + size_t length = 0; - // clear incomplete bytes - size_t fast_size = size - size % 3; - for (size_t i = 0; i < fast_size;) { - uint32_t octet_a = static_cast(bin[i++]); - uint32_t octet_b = static_cast(bin[i++]); - uint32_t octet_c = static_cast(bin[i++]); + padding() = default; + padding(size_t count, size_t length) : count(count), length(length) {} - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + padding operator+(const padding& p) { return padding(count + p.count, length + p.length); } - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += alphabet[(triple >> 1 * 6) & 0x3F]; - res += alphabet[(triple >> 0 * 6) & 0x3F]; - } + friend bool operator==(const padding& lhs, const padding& rhs) { + return lhs.count == rhs.count && lhs.length == rhs.length; + } + }; + + inline padding count_padding(const std::string& base, const std::vector& fills) { + for (const auto& fill : fills) { + if (base.size() < fill.size()) continue; + // Does the end of the input exactly match the fill pattern? + if (base.substr(base.size() - fill.size()) == fill) { + return padding{1, fill.length()} + + count_padding(base.substr(0, base.size() - fill.size()), fills); + } + } - if (fast_size == size) return res; - - size_t mod = size % 3; - - uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; - uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; - uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; - - uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; - - switch (mod) { - case 1: - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += fill; - res += fill; - break; - case 2: - res += alphabet[(triple >> 3 * 6) & 0x3F]; - res += alphabet[(triple >> 2 * 6) & 0x3F]; - res += alphabet[(triple >> 1 * 6) & 0x3F]; - res += fill; - break; - default: break; + return {}; } - return res; - } + inline std::string encode(const std::string& bin, const std::array& alphabet, + const std::string& fill) { + size_t size = bin.size(); + std::string res; + + // clear incomplete bytes + size_t fast_size = size - size % 3; + for (size_t i = 0; i < fast_size;) { + uint32_t octet_a = static_cast(bin[i++]); + uint32_t octet_b = static_cast(bin[i++]); + uint32_t octet_c = static_cast(bin[i++]); + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += alphabet[(triple >> 0 * 6) & 0x3F]; + } + + if (fast_size == size) return res; - static std::string decode(const std::string& base, const std::array& alphabet, - const std::string& fill) { - size_t size = base.size(); - - size_t fill_cnt = 0; - while (size > fill.size()) { - if (base.substr(size - fill.size(), fill.size()) == fill) { - fill_cnt++; - size -= fill.size(); - if (fill_cnt > 2) throw std::runtime_error("Invalid input: too much fill"); - } else + size_t mod = size % 3; + + uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + switch (mod) { + case 1: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += fill; + res += fill; break; + case 2: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += fill; + break; + default: break; + } + + return res; } - if ((size + fill_cnt) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size"); + inline std::string decode(const std::string& base, const std::array& alphabet, + const std::vector& fill) { + const auto pad = count_padding(base, fill); + if (pad.count > 2) throw std::runtime_error("Invalid input: too much fill"); - size_t out_size = size / 4 * 3; - std::string res; - res.reserve(out_size); + const size_t size = base.size() - pad.length; + if ((size + pad.count) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size"); - auto get_sextet = [&](size_t offset) { - for (size_t i = 0; i < alphabet.size(); i++) { - if (alphabet[i] == base[offset]) return static_cast(i); + size_t out_size = size / 4 * 3; + std::string res; + res.reserve(out_size); + + auto get_sextet = [&](size_t offset) { return alphabet::index(alphabet, base[offset]); }; + + size_t fast_size = size - size % 4; + for (size_t i = 0; i < fast_size;) { + uint32_t sextet_a = get_sextet(i++); + uint32_t sextet_b = get_sextet(i++); + uint32_t sextet_c = get_sextet(i++); + uint32_t sextet_d = get_sextet(i++); + + uint32_t triple = + (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); + + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + res += static_cast((triple >> 0 * 8) & 0xFFU); } - throw std::runtime_error("Invalid input: not within alphabet"); - }; - size_t fast_size = size - size % 4; - for (size_t i = 0; i < fast_size;) { - uint32_t sextet_a = get_sextet(i++); - uint32_t sextet_b = get_sextet(i++); - uint32_t sextet_c = get_sextet(i++); - uint32_t sextet_d = get_sextet(i++); + if (pad.count == 0) return res; + + uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); - uint32_t triple = (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); + switch (pad.count) { + case 1: + triple |= (get_sextet(fast_size + 2) << 1 * 6); + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + break; + case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; + default: break; + } - res += static_cast((triple >> 2 * 8) & 0xFFU); - res += static_cast((triple >> 1 * 8) & 0xFFU); - res += static_cast((triple >> 0 * 8) & 0xFFU); + return res; } - if (fill_cnt == 0) return res; + inline std::string decode(const std::string& base, const std::array& alphabet, + const std::string& fill) { + return decode(base, alphabet, std::vector{fill}); + } - uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); + inline std::string pad(const std::string& base, const std::string& fill) { + std::string padding; + switch (base.size() % 4) { + case 1: padding += fill; JWT_FALLTHROUGH; + case 2: padding += fill; JWT_FALLTHROUGH; + case 3: padding += fill; JWT_FALLTHROUGH; + default: break; + } - switch (fill_cnt) { - case 1: - triple |= (get_sextet(fast_size + 2) << 1 * 6); - res += static_cast((triple >> 2 * 8) & 0xFFU); - res += static_cast((triple >> 1 * 8) & 0xFFU); - break; - case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; - default: break; + return base + padding; } - return res; - } - - static std::string pad(const std::string& base, const std::string& fill) { - std::string padding; - switch (base.size() % 4) { - case 1: padding += fill; JWT_FALLTHROUGH; - case 2: padding += fill; JWT_FALLTHROUGH; - case 3: padding += fill; JWT_FALLTHROUGH; - default: break; + inline std::string trim(const std::string& base, const std::string& fill) { + auto pos = base.find(fill); + return base.substr(0, pos); } + } // namespace details - return base + padding; + template + std::string encode(const std::string& bin) { + return details::encode(bin, T::data(), T::fill()); } - - static std::string trim(const std::string& base, const std::string& fill) { - auto pos = base.find(fill); - return base.substr(0, pos); + template + std::string decode(const std::string& base) { + return details::decode(base, T::data(), T::fill()); + } + template + std::string pad(const std::string& base) { + return details::pad(base, T::fill()); + } + template + std::string trim(const std::string& base) { + return details::trim(base, T::fill()); } - }; + } // namespace base } // namespace jwt #endif diff --git a/Development/third_party/jwt-cpp/jwt.h b/Development/third_party/jwt-cpp/jwt.h index 211305e01..ec63b64c7 100644 --- a/Development/third_party/jwt-cpp/jwt.h +++ b/Development/third_party/jwt-cpp/jwt.h @@ -18,11 +18,12 @@ #include #include #include +#include #include #include #include -#include +#include #include #include #include @@ -34,6 +35,10 @@ #include #include +#if __cplusplus > 201103L +#include +#endif + #if __cplusplus >= 201402L #ifdef __has_include #if __has_include() @@ -53,8 +58,12 @@ #endif #if defined(LIBRESSL_VERSION_NUMBER) +#if LIBRESSL_VERSION_NUMBER >= 0x3050300fL +#define JWT_OPENSSL_1_1_0 +#else #define JWT_OPENSSL_1_0_0 #endif +#endif #if defined(LIBWOLFSSL_VERSION_HEX) #define JWT_OPENSSL_1_1_1 @@ -353,15 +362,8 @@ namespace jwt { } } } // namespace error - - // FIXME: Remove - // Keep backward compat at least for a couple of revisions - using error::ecdsa_exception; - using error::rsa_exception; - using error::signature_generation_exception; - using error::signature_verification_exception; - using error::token_verification_exception; } // namespace jwt + namespace std { template<> struct is_error_code_enum : true_type {}; @@ -374,6 +376,7 @@ namespace std { template<> struct is_error_code_enum : true_type {}; } // namespace std + namespace jwt { /** * \brief A collection for working with certificates @@ -383,24 +386,119 @@ namespace jwt { * you maybe need to extract the modulus and exponent of an RSA Public Key. */ namespace helper { + /** + * \brief Handle class for EVP_PKEY structures + * + * Starting from OpenSSL 1.1.0, EVP_PKEY has internal reference counting. This handle class allows + * jwt-cpp to leverage that and thus safe an allocation for the control block in std::shared_ptr. + * The handle uses shared_ptr as a fallback on older versions. The behaviour should be identical between both. + */ + class evp_pkey_handle { + public: + constexpr evp_pkey_handle() noexcept = default; +#ifdef JWT_OPENSSL_1_0_0 + /** + * \brief Construct a new handle. The handle takes ownership of the key. + * \param key The key to store + */ + explicit evp_pkey_handle(EVP_PKEY* key) { m_key = std::shared_ptr(key, EVP_PKEY_free); } + + EVP_PKEY* get() const noexcept { return m_key.get(); } + bool operator!() const noexcept { return m_key == nullptr; } + explicit operator bool() const noexcept { return m_key != nullptr; } + + private: + std::shared_ptr m_key{nullptr}; +#else + /** + * \brief Construct a new handle. The handle takes ownership of the key. + * \param key The key to store + */ + explicit constexpr evp_pkey_handle(EVP_PKEY* key) noexcept : m_key{key} {} + evp_pkey_handle(const evp_pkey_handle& other) : m_key{other.m_key} { + if (m_key != nullptr && EVP_PKEY_up_ref(m_key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); + } +// C++11 requires the body of a constexpr constructor to be empty +#if __cplusplus >= 201402L + constexpr +#endif + evp_pkey_handle(evp_pkey_handle&& other) noexcept + : m_key{other.m_key} { + other.m_key = nullptr; + } + evp_pkey_handle& operator=(const evp_pkey_handle& other) { + if (&other == this) return *this; + decrement_ref_count(m_key); + m_key = other.m_key; + increment_ref_count(m_key); + return *this; + } + evp_pkey_handle& operator=(evp_pkey_handle&& other) noexcept { + if (&other == this) return *this; + decrement_ref_count(m_key); + m_key = other.m_key; + other.m_key = nullptr; + return *this; + } + evp_pkey_handle& operator=(EVP_PKEY* key) { + decrement_ref_count(m_key); + m_key = key; + increment_ref_count(m_key); + return *this; + } + ~evp_pkey_handle() noexcept { decrement_ref_count(m_key); } + + EVP_PKEY* get() const noexcept { return m_key; } + bool operator!() const noexcept { return m_key == nullptr; } + explicit operator bool() const noexcept { return m_key != nullptr; } + + private: + EVP_PKEY* m_key{nullptr}; + + static void increment_ref_count(EVP_PKEY* key) { + if (key != nullptr && EVP_PKEY_up_ref(key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); + } + static void decrement_ref_count(EVP_PKEY* key) noexcept { + if (key != nullptr) EVP_PKEY_free(key); + } +#endif + }; + + inline std::unique_ptr make_mem_buf_bio() { + return std::unique_ptr(BIO_new(BIO_s_mem()), BIO_free_all); + } + + inline std::unique_ptr make_mem_buf_bio(const std::string& data) { + return std::unique_ptr( +#if OPENSSL_VERSION_NUMBER <= 0x10100003L + BIO_new_mem_buf(const_cast(data.data()), static_cast(data.size())), BIO_free_all +#else + BIO_new_mem_buf(data.data(), static_cast(data.size())), BIO_free_all +#endif + ); + } + + inline std::unique_ptr make_evp_md_ctx() { + return +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr(EVP_MD_CTX_create(), &EVP_MD_CTX_destroy); +#else + std::unique_ptr(EVP_MD_CTX_new(), &EVP_MD_CTX_free); +#endif + } + /** * \brief Extract the public key of a pem certificate * * \param certstr String containing the certificate encoded as pem * \param pw Password used to decrypt certificate (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error ocurred) */ inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw, std::error_code& ec) { ec.clear(); -#if OPENSSL_VERSION_NUMBER <= 0x10100003L - std::unique_ptr certbio( - BIO_new_mem_buf(const_cast(certstr.data()), static_cast(certstr.size())), BIO_free_all); -#else - std::unique_ptr certbio( - BIO_new_mem_buf(certstr.data(), static_cast(certstr.size())), BIO_free_all); -#endif - std::unique_ptr keybio(BIO_new(BIO_s_mem()), BIO_free_all); + auto certbio = make_mem_buf_bio(certstr); + auto keybio = make_mem_buf_bio(); if (!certbio || !keybio) { ec = error::rsa_error::create_mem_bio_failed; return {}; @@ -467,7 +565,7 @@ namespace jwt { std::unique_ptr cert( d2i_X509(NULL, &c_str, static_cast(decodedStr.size())), X509_free); - std::unique_ptr certbio(BIO_new(BIO_s_mem()), BIO_free_all); + auto certbio = make_mem_buf_bio(); if (!cert || !certbio) { ec = error::rsa_error::create_mem_bio_failed; return {}; @@ -551,38 +649,34 @@ namespace jwt { * \param password Password used to decrypt certificate (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline std::shared_ptr load_public_key_from_string(const std::string& key, - const std::string& password, std::error_code& ec) { + inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { ec.clear(); - std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + auto pubkey_bio = make_mem_buf_bio(); if (!pubkey_bio) { ec = error::rsa_error::create_mem_bio_failed; - return nullptr; + return {}; } if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { auto epkey = helper::extract_pubkey_from_cert(key, password, ec); - if (ec) return nullptr; + if (ec) return {}; const int len = static_cast(epkey.size()); if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return nullptr; + return {}; } } else { const int len = static_cast(key.size()); if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return nullptr; + return {}; } } - std::shared_ptr pkey( - PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, - (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` - EVP_PKEY_free); - if (!pkey) { - ec = error::rsa_error::load_key_bio_read; - return nullptr; - } + evp_pkey_handle pkey(PEM_read_bio_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` + if (!pkey) ec = error::rsa_error::load_key_bio_read; return pkey; } @@ -595,8 +689,7 @@ namespace jwt { * \param password Password used to decrypt certificate or key (leave empty if not encrypted) * \throw rsa_exception if an error occurred */ - inline std::shared_ptr load_public_key_from_string(const std::string& key, - const std::string& password = "") { + inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password = "") { std::error_code ec; auto res = load_public_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -610,25 +703,21 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline std::shared_ptr - load_private_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { - std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + auto privkey_bio = make_mem_buf_bio(); if (!privkey_bio) { ec = error::rsa_error::create_mem_bio_failed; - return nullptr; + return {}; } const int len = static_cast(key.size()); if (BIO_write(privkey_bio.get(), key.data(), len) != len) { ec = error::rsa_error::load_key_bio_write; - return nullptr; - } - std::shared_ptr pkey( - PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), - EVP_PKEY_free); - if (!pkey) { - ec = error::rsa_error::load_key_bio_read; - return nullptr; + return {}; } + evp_pkey_handle pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); + if (!pkey) ec = error::rsa_error::load_key_bio_read; return pkey; } @@ -639,8 +728,7 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \throw rsa_exception if an error occurred */ - inline std::shared_ptr load_private_key_from_string(const std::string& key, - const std::string& password = "") { + inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password = "") { std::error_code ec; auto res = load_private_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -656,38 +744,34 @@ namespace jwt { * \param password Password used to decrypt certificate (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline std::shared_ptr - load_public_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { + inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { ec.clear(); - std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + auto pubkey_bio = make_mem_buf_bio(); if (!pubkey_bio) { ec = error::ecdsa_error::create_mem_bio_failed; - return nullptr; + return {}; } if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { auto epkey = helper::extract_pubkey_from_cert(key, password, ec); - if (ec) return nullptr; + if (ec) return {}; const int len = static_cast(epkey.size()); if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { ec = error::ecdsa_error::load_key_bio_write; - return nullptr; + return {}; } } else { const int len = static_cast(key.size()); if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { ec = error::ecdsa_error::load_key_bio_write; - return nullptr; + return {}; } } - std::shared_ptr pkey( - PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, - (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` - EVP_PKEY_free); - if (!pkey) { - ec = error::ecdsa_error::load_key_bio_read; - return nullptr; - } + evp_pkey_handle pkey(PEM_read_bio_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` + if (!pkey) ec = error::ecdsa_error::load_key_bio_read; return pkey; } @@ -700,8 +784,8 @@ namespace jwt { * \param password Password used to decrypt certificate or key (leave empty if not encrypted) * \throw ecdsa_exception if an error occurred */ - inline std::shared_ptr load_public_ec_key_from_string(const std::string& key, - const std::string& password = "") { + inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, + const std::string& password = "") { std::error_code ec; auto res = load_public_ec_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -715,25 +799,21 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \param ec error_code for error_detection (gets cleared if no error occures) */ - inline std::shared_ptr - load_private_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { - std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + auto privkey_bio = make_mem_buf_bio(); if (!privkey_bio) { ec = error::ecdsa_error::create_mem_bio_failed; - return nullptr; + return {}; } const int len = static_cast(key.size()); if (BIO_write(privkey_bio.get(), key.data(), len) != len) { ec = error::ecdsa_error::load_key_bio_write; - return nullptr; - } - std::shared_ptr pkey( - PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), - EVP_PKEY_free); - if (!pkey) { - ec = error::ecdsa_error::load_key_bio_read; - return nullptr; + return {}; } + evp_pkey_handle pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); + if (!pkey) ec = error::ecdsa_error::load_key_bio_read; return pkey; } @@ -744,8 +824,8 @@ namespace jwt { * \param password Password used to decrypt key (leave empty if not encrypted) * \throw ecdsa_exception if an error occurred */ - inline std::shared_ptr load_private_ec_key_from_string(const std::string& key, - const std::string& password = "") { + inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, + const std::string& password = "") { std::error_code ec; auto res = load_private_ec_key_from_string(key, password, ec); error::throw_if_error(ec); @@ -906,7 +986,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw rsa_exception(error::rsa_error::no_key_provided); + throw error::rsa_exception(error::rsa_error::no_key_provided); } /** * Sign jwt data @@ -916,11 +996,7 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -953,11 +1029,7 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -985,7 +1057,7 @@ namespace jwt { private: /// OpenSSL structure containing converted keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// Hash generator const EVP_MD* (*md)(); /// algorithm's name @@ -1016,13 +1088,13 @@ namespace jwt { pkey = helper::load_public_ec_key_from_string(public_key, public_key_password); check_public_key(pkey.get()); } else { - throw ecdsa_exception(error::ecdsa_error::no_key_provided); + throw error::ecdsa_exception(error::ecdsa_error::no_key_provided); } - if (!pkey) throw ecdsa_exception(error::ecdsa_error::invalid_key); + if (!pkey) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); size_t keysize = EVP_PKEY_bits(pkey.get()); if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) - throw ecdsa_exception(error::ecdsa_error::invalid_key_size); + throw error::ecdsa_exception(error::ecdsa_error::invalid_key_size); } /** @@ -1033,11 +1105,7 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1077,11 +1145,7 @@ namespace jwt { std::string der_signature = p1363_to_der_signature(signature, ec); if (ec) { return; } -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_create(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1122,12 +1186,14 @@ namespace jwt { #ifdef JWT_OPENSSL_3_0 std::unique_ptr ctx( EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); - if (!ctx) { throw ecdsa_exception(error::ecdsa_error::create_context_failed); } - if (EVP_PKEY_public_check(ctx.get()) != 1) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } + if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_public_check(ctx.get()) != 1) { + throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + } #else std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); - if (!eckey) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } - if (EC_KEY_check_key(eckey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); + if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); #endif } @@ -1135,19 +1201,22 @@ namespace jwt { #ifdef JWT_OPENSSL_3_0 std::unique_ptr ctx( EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); - if (!ctx) { throw ecdsa_exception(error::ecdsa_error::create_context_failed); } - if (EVP_PKEY_private_check(ctx.get()) != 1) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } + if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_private_check(ctx.get()) != 1) { + throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + } #else std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); - if (!eckey) { throw ecdsa_exception(error::ecdsa_error::invalid_key); } - if (EC_KEY_check_key(eckey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); + if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); #endif } std::string der_to_p1363_signature(const std::string& der_signature, std::error_code& ec) const { const unsigned char* possl_signature = reinterpret_cast(der_signature.data()); std::unique_ptr sig( - d2i_ECDSA_SIG(nullptr, &possl_signature, der_signature.length()), ECDSA_SIG_free); + d2i_ECDSA_SIG(nullptr, &possl_signature, static_cast(der_signature.length())), + ECDSA_SIG_free); if (!sig) { ec = error::signature_generation_error::signature_decoding_failed; return {}; @@ -1209,7 +1278,7 @@ namespace jwt { } /// OpenSSL struct containing keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// Hash generator function const EVP_MD* (*md)(); /// algorithm's name @@ -1246,7 +1315,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); + throw error::ecdsa_exception(error::ecdsa_error::load_key_bio_read); } /** * Sign jwt data @@ -1256,12 +1325,7 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr ctx(EVP_MD_CTX_create(), - &EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1309,12 +1373,7 @@ namespace jwt { */ void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr ctx(EVP_MD_CTX_create(), - &EVP_MD_CTX_destroy); -#else - std::unique_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); -#endif + auto ctx = helper::make_evp_md_ctx(); if (!ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1355,7 +1414,7 @@ namespace jwt { private: /// OpenSSL struct containing keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// algorithm's name const std::string alg_name; }; @@ -1381,7 +1440,7 @@ namespace jwt { } else if (!public_key.empty()) { pkey = helper::load_public_key_from_string(public_key, public_key_password); } else - throw rsa_exception(error::rsa_error::no_key_provided); + throw error::rsa_exception(error::rsa_error::no_key_provided); } /** @@ -1392,12 +1451,7 @@ namespace jwt { */ std::string sign(const std::string& data, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr md_ctx(EVP_MD_CTX_create(), - &EVP_MD_CTX_destroy); -#else - std::unique_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); -#endif + auto md_ctx = helper::make_evp_md_ctx(); if (!md_ctx) { ec = error::signature_generation_error::create_context_failed; return {}; @@ -1446,12 +1500,7 @@ namespace jwt { void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { ec.clear(); -#ifdef JWT_OPENSSL_1_0_0 - std::unique_ptr md_ctx(EVP_MD_CTX_create(), - &EVP_MD_CTX_destroy); -#else - std::unique_ptr md_ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); -#endif + auto md_ctx = helper::make_evp_md_ctx(); if (!md_ctx) { ec = error::signature_verification_error::create_context_failed; return; @@ -1491,7 +1540,7 @@ namespace jwt { private: /// OpenSSL structure containing keys - std::shared_ptr pkey; + helper::evp_pkey_handle pkey; /// Hash generator function const EVP_MD* (*md)(); /// algorithm's name @@ -1764,9 +1813,6 @@ namespace jwt { #ifdef __cpp_lib_experimental_detect template class _Op, typename... _Args> using is_detected = std::experimental::is_detected<_Op, _Args...>; - - template class _Op, typename... _Args> - using is_detected_t = std::experimental::detected_t<_Op, _Args...>; #else struct nonesuch { nonesuch() = delete; @@ -1792,139 +1838,58 @@ namespace jwt { template class Op, class... Args> using is_detected = typename detector::value; - - template class Op, class... Args> - using is_detected_t = typename detector::type; #endif - template - using get_type_function = decltype(traits_type::get_type); + template + using is_signature = typename std::is_same; - template - using is_get_type_signature = - typename std::is_same, json::type(const value_type&)>; + template class Op, typename Signature> + struct is_function_signature_detected { + using type = Op; + static constexpr auto value = is_detected::value && std::is_function::value && + is_signature::value; + }; template struct supports_get_type { - static constexpr auto value = is_detected::value && - std::is_function>::value && - is_get_type_signature::value; - }; - - template - using as_object_function = decltype(traits_type::as_object); - - template - using is_as_object_signature = - typename std::is_same, object_type(const value_type&)>; - - template - struct supports_as_object { - static constexpr auto value = std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_object_signature::value; - }; - - template - using as_array_function = decltype(traits_type::as_array); - - template - using is_as_array_signature = - typename std::is_same, array_type(const value_type&)>; - - template - struct supports_as_array { - static constexpr auto value = std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_array_signature::value; - }; - - template - using as_string_function = decltype(traits_type::as_string); - - template - using is_as_string_signature = - typename std::is_same, string_type(const value_type&)>; - - template - struct supports_as_string { - static constexpr auto value = std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_string_signature::value; - }; - - template - using as_number_function = decltype(traits_type::as_number); - - template - using is_as_number_signature = - typename std::is_same, number_type(const value_type&)>; - - template - struct supports_as_number { - static constexpr auto value = std::is_floating_point::value && - std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_number_signature::value; - }; - - template - using as_integer_function = decltype(traits_type::as_int); + template + using get_type_t = decltype(T::get_type); - template - using is_as_integer_signature = - typename std::is_same, integer_type(const value_type&)>; - - template - struct supports_as_integer { - static constexpr auto value = std::is_signed::value && - !std::is_floating_point::value && - std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_integer_signature::value; - }; + static constexpr auto value = + is_function_signature_detected::value; - template - using as_boolean_function = decltype(traits_type::as_bool); + // Internal assertions for better feedback + static_assert(value, "traits implementation must provide `jwt::json::type get_type(const value_type&)`"); + }; + +#define JWT_CPP_JSON_TYPE_TYPE(TYPE) json_##TYPE_type +#define JWT_CPP_AS_TYPE_T(TYPE) as_##TYPE_t +#define JWT_CPP_SUPPORTS_AS(TYPE) \ + template \ + struct supports_as_##TYPE { \ + template \ + using JWT_CPP_AS_TYPE_T(TYPE) = decltype(T::as_##TYPE); \ + \ + static constexpr auto value = \ + is_function_signature_detected::value; \ + \ + static_assert(value, "traits implementation must provide `" #TYPE "_type as_" #TYPE "(const value_type&)`"); \ + } - template - using is_as_boolean_signature = - typename std::is_same, boolean_type(const value_type&)>; + JWT_CPP_SUPPORTS_AS(object); + JWT_CPP_SUPPORTS_AS(array); + JWT_CPP_SUPPORTS_AS(string); + JWT_CPP_SUPPORTS_AS(number); + JWT_CPP_SUPPORTS_AS(integer); + JWT_CPP_SUPPORTS_AS(boolean); - template - struct supports_as_boolean { - static constexpr auto value = std::is_convertible::value && - std::is_constructible::value && - is_detected::value && - std::is_function>::value && - is_as_boolean_signature::value; - }; +#undef JWT_CPP_JSON_TYPE_TYPE +#undef JWT_CPP_AS_TYPE_T +#undef JWT_CPP_SUPPORTS_AS template struct is_valid_traits { - // Internal assertions for better feedback - static_assert(supports_get_type::value, - "traits must provide `jwt::json::type get_type(const value_type&)`"); - static_assert(supports_as_object::value, - "traits must provide `object_type as_object(const value_type&)`"); - static_assert(supports_as_array::value, - "traits must provide `array_type as_array(const value_type&)`"); - static_assert(supports_as_string::value, - "traits must provide `string_type as_string(const value_type&)`"); - static_assert(supports_as_number::value, - "traits must provide `number_type as_number(const value_type&)`"); - static_assert( - supports_as_integer::value, - "traits must provide `integer_type as_int(const value_type&)`"); - static_assert( - supports_as_boolean::value, - "traits must provide `boolean_type as_bool(const value_type&)`"); - static constexpr auto value = supports_get_type::value && supports_as_object::value && @@ -1945,76 +1910,41 @@ namespace jwt { // TODO(prince-chrismc): Stream operators }; - template - using has_mapped_type = typename traits_type::mapped_type; - - template - using has_key_type = typename traits_type::key_type; - - template - using has_value_type = typename traits_type::value_type; - - template - using has_iterator = typename object_type::iterator; - - template - using has_const_iterator = typename object_type::const_iterator; - - template - using is_begin_signature = - typename std::is_same().begin()), has_iterator>; - - template - using is_begin_const_signature = - typename std::is_same().begin()), has_const_iterator>; - - template - struct supports_begin { - static constexpr auto value = - is_detected::value && is_detected::value && - is_begin_signature::value && is_begin_const_signature::value; - }; - - template - using is_end_signature = - typename std::is_same().end()), has_iterator>; - - template - using is_end_const_signature = - typename std::is_same().end()), has_const_iterator>; + // https://stackoverflow.com/a/53967057/8480874 + template + struct is_iterable : std::false_type {}; - template - struct supports_end { - static constexpr auto value = - is_detected::value && is_detected::value && - is_end_signature::value && is_end_const_signature::value; + template + struct is_iterable())), decltype(std::end(std::declval())), +#if __cplusplus > 201402L + decltype(std::cbegin(std::declval())), decltype(std::cend(std::declval())) +#else + decltype(std::begin(std::declval())), + decltype(std::end(std::declval())) +#endif + >> : std::true_type { }; - template - using is_count_signature = typename std::is_integral().count(std::declval()))>; +#if __cplusplus > 201703L + template + inline constexpr bool is_iterable_v = is_iterable::value; +#endif template - struct has_subcription_operator { - template - struct sfinae_true : std::true_type {}; - - template - static auto test_operator_plus(int) - -> sfinae_true().operator[](std::declval()))>; - template - static auto test_operator_plus(long) -> std::false_type; - - static constexpr auto value = decltype(test_operator_plus(0)){}; - }; + using is_count_signature = typename std::is_integral().count( + std::declval()))>; - template - struct is_subcription_operator_signature { - static constexpr auto has_subscription_operator = has_subcription_operator::value; - static_assert(has_subscription_operator, - "object_type must implementate the subscription operator '[]' for this library"); + template + struct is_subcription_operator_signature : std::false_type {}; - static constexpr auto value = has_subscription_operator; + template + struct is_subcription_operator_signature< + object_type, string_type, + void_t().operator[](std::declval()))>> : std::true_type { + // TODO(prince-chrismc): I am not convienced this is meaningful anymore + static_assert( + value, + "object_type must implementate the subscription operator '[]' taking string_type as an arguement"); }; template @@ -2024,25 +1954,37 @@ namespace jwt { template struct is_valid_json_object { + template + using mapped_type_t = typename T::mapped_type; + template + using key_type_t = typename T::key_type; + template + using iterator_t = typename T::iterator; + template + using const_iterator_t = typename T::const_iterator; + static constexpr auto value = - is_detected::value && + std::is_constructible::value && + is_detected::value && std::is_same::value && - is_detected::value && + is_detected::value && (std::is_same::value || std::is_constructible::value) && - supports_begin::value && supports_end::value && - is_count_signature::value && - is_subcription_operator_signature::value && + is_detected::value && is_detected::value && + is_iterable::value && is_count_signature::value && + is_subcription_operator_signature::value && is_at_const_signature::value; - - static constexpr auto supports_claims_transform = - value && is_detected::value && - std::is_same>::value; }; template struct is_valid_json_array { - static constexpr auto value = std::is_same::value; + template + using value_type_t = typename T::value_type; + + static constexpr auto value = std::is_constructible::value && + is_iterable::value && + is_detected::value && + std::is_same::value; }; template @@ -2056,42 +1998,46 @@ namespace jwt { typename std::is_same().substr(std::declval())), string_type>; - template - struct has_operate_plus_method { // https://stackoverflow.com/a/9154394/8480874 - template - struct sfinae_true : std::true_type {}; - - template - static auto test_operator_plus(int) - -> sfinae_true().operator+(std::declval()))>; - template - static auto test_operator_plus(long) -> std::false_type; - - static constexpr auto value = decltype(test_operator_plus(0)){}; - }; - template using is_std_operate_plus_signature = typename std::is_same(), std::declval())), string_type>; - template + template struct is_valid_json_string { static constexpr auto substr = is_substr_start_end_index_signature::value && is_substr_start_index_signature::value; static_assert(substr, "string_type must have a substr method taking only a start index and an overload " "taking a start and end index, both must return a string_type"); - static constexpr auto operator_plus = - has_operate_plus_method::value || is_std_operate_plus_signature::value; + static constexpr auto operator_plus = is_std_operate_plus_signature::value; static_assert(operator_plus, "string_type must have a '+' operator implemented which returns the concatenated string"); - static constexpr auto value = substr && operator_plus; + static constexpr auto value = + std::is_constructible::value && substr && operator_plus; + }; + + template + struct is_valid_json_number { + static constexpr auto value = + std::is_floating_point::value && std::is_constructible::value; + }; + + template + struct is_valid_json_integer { + static constexpr auto value = std::is_signed::value && + !std::is_floating_point::value && + std::is_constructible::value; + }; + template + struct is_valid_json_boolean { + static constexpr auto value = std::is_convertible::value && + std::is_constructible::value; }; - template + template struct is_valid_json_types { // Internal assertions for better feedback static_assert(is_valid_json_value::value, @@ -2101,10 +2047,13 @@ namespace jwt { static_assert(is_valid_json_array::value, "array_type must be a container of value_type"); - static constexpr auto value = is_valid_json_object::value && - is_valid_json_value::value && + static constexpr auto value = is_valid_json_value::value && + is_valid_json_object::value && is_valid_json_array::value && - is_valid_json_string::value; + is_valid_json_string::value && + is_valid_json_number::value && + is_valid_json_integer::value && + is_valid_json_boolean::value; }; } // namespace details @@ -2129,9 +2078,10 @@ namespace jwt { "string_type must be a std::string, convertible to a std::string, or construct a std::string."); static_assert( - details::is_valid_json_types::value, + details::is_valid_json_types::value, "must staisfy json container requirements"); static_assert(details::is_valid_traits::value, "traits must satisfy requirements"); @@ -2189,11 +2139,18 @@ namespace jwt { typename json_traits::string_type as_string() const { return json_traits::as_string(val); } /** - * Get the contained JSON value as a date + * \brief Get the contained JSON value as a date + * + * If the value is a decimal, it is rounded up to the closest integer + * * \return content as date * \throw std::bad_cast Content was not a date */ - date as_date() const { return std::chrono::system_clock::from_time_t(as_int()); } + date as_date() const { + using std::chrono::system_clock; + if (get_type() == json::type::number) return system_clock::from_time_t(std::round(as_number())); + return system_clock::from_time_t(as_integer()); + } /** * Get the contained JSON value as an array @@ -2220,14 +2177,14 @@ namespace jwt { * \return content as int * \throw std::bad_cast Content was not an int */ - typename json_traits::integer_type as_int() const { return json_traits::as_int(val); } + typename json_traits::integer_type as_integer() const { return json_traits::as_integer(val); } /** * Get the contained JSON value as a bool * \return content as bool * \throw std::bad_cast Content was not a bool */ - typename json_traits::boolean_type as_bool() const { return json_traits::as_bool(val); } + typename json_traits::boolean_type as_boolean() const { return json_traits::as_boolean(val); } /** * Get the contained JSON value as a number @@ -2254,10 +2211,8 @@ namespace jwt { namespace details { template - class map_of_claims { + struct map_of_claims { typename json_traits::object_type claims; - - public: using basic_claim_t = basic_claim; using iterator = typename json_traits::object_type::iterator; using const_iterator = typename json_traits::object_type::const_iterator; @@ -2311,21 +2266,6 @@ namespace jwt { if (!has_claim(name)) throw error::claim_not_present_exception(); return basic_claim_t{claims.at(name)}; } - - std::unordered_map get_claims() const { - static_assert( - details::is_valid_json_object::supports_claims_transform, - "currently there is a limitation on the internal implemantation of the `object_type` to have an " - "`std::pair` like `value_type`"); - - std::unordered_map res; - std::transform(claims.begin(), claims.end(), std::inserter(res, res.end()), - [](const typename json_traits::object_type::value_type& val) { - return std::make_pair(val.first, basic_claim_t{val.second}); - }); - return res; - } }; } // namespace details @@ -2530,7 +2470,7 @@ namespace jwt { class decoded_jwt : public header, public payload { protected: /// Unmodifed token, as passed to constructor - const typename json_traits::string_type token; + typename json_traits::string_type token; /// Header part decoded from base64 typename json_traits::string_type header; /// Unmodified header part in base64 @@ -2626,19 +2566,15 @@ namespace jwt { */ const typename json_traits::string_type& get_signature_base64() const noexcept { return signature_base64; } /** - * Get all payload claims + * Get all payload as JSON object * \return map of claims */ - std::unordered_map get_payload_claims() const { - return this->payload_claims.get_claims(); - } + typename json_traits::object_type get_payload_json() const { return this->payload_claims.claims; } /** - * Get all header claims + * Get all header as JSON object * \return map of claims */ - std::unordered_map get_header_claims() const { - return this->header_claims.get_claims(); - } + typename json_traits::object_type get_header_json() const { return this->header_claims.claims; } /** * Get a payload claim by name * @@ -2952,8 +2888,8 @@ namespace jwt { if (ec) return; const bool matches = [&]() { switch (expected.get_type()) { - case json::type::boolean: return expected.as_bool() == jc.as_bool(); - case json::type::integer: return expected.as_int() == jc.as_int(); + case json::type::boolean: return expected.as_boolean() == jc.as_boolean(); + case json::type::integer: return expected.as_integer() == jc.as_integer(); case json::type::number: return expected.as_number() == jc.as_number(); case json::type::string: return expected.as_string() == jc.as_string(); case json::type::array: @@ -3053,11 +2989,18 @@ namespace jwt { } static std::string to_lower_unicode(const std::string& str, const std::locale& loc) { +#if __cplusplus > 201103L std::wstring_convert, wchar_t> conv; auto wide = conv.from_bytes(str); auto& f = std::use_facet>(loc); f.tolower(&wide[0], &wide[0] + wide.size()); return conv.to_bytes(wide); +#else + std::string result; + std::transform(str.begin(), str.end(), std::back_inserter(result), + [&loc](unsigned char c) { return std::tolower(c, loc); }); + return result; +#endif } }; } // namespace verify_ops @@ -3501,6 +3444,12 @@ namespace jwt { } bool empty() const noexcept { return jwk_claims.empty(); } + + /** + * Get all jwk claims + * \return Map of claims + */ + typename json_traits::object_type get_claims() const { return this->jwk_claims.claims; } }; /** diff --git a/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h b/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h index 10b9a5af2..c324075f8 100644 --- a/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h +++ b/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h @@ -77,6 +77,12 @@ namespace jwt { inline jwks parse_jwks(const traits::nlohmann_json::string_type& token) { return jwks(token); } + + /** + * This type is the specialization of the \ref verify_ops::verify_context class which + * uses the standard template types. + */ + using verify_context = verify_ops::verify_context; } // namespace jwt #endif // JWT_CPP_NLOHMANN_JSON_DEFAULTS_H diff --git a/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h b/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h index 23c2d7d10..7cf486902 100644 --- a/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h +++ b/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h @@ -46,7 +46,7 @@ namespace jwt { return val.get(); } - static int64_t as_int(const json& val) { + static int64_t as_integer(const json& val) { switch (val.type()) { case json::value_t::number_integer: case json::value_t::number_unsigned: return val.get(); @@ -54,7 +54,7 @@ namespace jwt { } } - static bool as_bool(const json& val) { + static bool as_boolean(const json& val) { if (val.type() != json::value_t::boolean) throw std::bad_cast(); return val.get(); } From f441cd63ff434c86a298fedd57ba2b61eb8ee3e2 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 12 Oct 2023 18:39:41 +0100 Subject: [PATCH 084/121] Add client_id and azp validation --- Development/nmos/json_fields.h | 3 +- Development/nmos/jwt_validator_impl.cpp | 90 +++++++++++++++++++------ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/Development/nmos/json_fields.h b/Development/nmos/json_fields.h index a637cb751..eb243b04d 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -288,10 +288,11 @@ namespace nmos const web::json::field_as_array keys{ U("keys") }; // use inside jwks const web::json::field_as_string software_id{ U("software_id") }; // OPTIONAL const web::json::field_as_string software_version{ U("software_version") }; // OPTIONAL - const web::json::field_as_string_or client_id{ U("client_id"),{} }; + const web::json::field_as_string_or client_id{ U("client_id"), {} }; const web::json::field_as_string client_secret{ U("client_secret") }; // OPTIONAL const web::json::field_as_integer client_id_issued_at{ U("client_id_issued_at") }; // OPTIONAL const web::json::field_as_integer_or client_secret_expires_at{ U("client_secret_expires_at"),0 }; + const web::json::field_as_string azp{ U("azp") }; // OPTIONAL // OpenID Connect extension const web::json::field_as_string registration_client_uri{ U("registration_client_uri") }; // OPTIONAL const web::json::field_as_string registration_access_token{ U("registration_access_token") }; // OPTIONAL diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index 7db1c1148..1ad8d26a4 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -235,11 +235,36 @@ namespace nmos } return true; }; - if (validate_scope && !verify_scope(scope)) + if (!verify_scope(scope)) { throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); } + // verify client_id and azp (optional) + auto verify_client_id = [&decoded_token]() + { + const auto client_id_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::client_id)); + const auto azp_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::azp)); + + if ((client_id_found && !azp_found) || (!client_id_found && azp_found)) + { + return true; + } + + if (client_id_found && + azp_found && + decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::client_id)).as_string() == decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::azp)).as_string()) + { + return true; + } + + return false; + }; + if (!verify_client_id()) + { + throw insufficient_scope_exception("missing client_id or azp, or client_id and azp are not matching"); + } + // verify Private Claims // x-nmos-* (Contains information particular to the NMOS API the token is intended for) @@ -274,18 +299,13 @@ namespace nmos }; // write accessible - if ((web::http::methods::POST == req.method()) - || (web::http::methods::PUT == req.method()) - || (web::http::methods::PATCH == req.method()) - || (web::http::methods::DEL == req.method())) + if (is_write_method(req.method())) { return accessible("write"); } // read accessible - if ((web::http::methods::OPTIONS == req.method()) - || (web::http::methods::GET == req.method()) - || (web::http::methods::HEAD == req.method())) + if (is_read_method(req.method())) { return accessible("read"); } @@ -298,15 +318,15 @@ namespace nmos { const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); - // extract from /x-nmos/// + // extract {path} from /x-nmos/{api name, the scope name}/{api version}/{path} auto extract_path = [req](const nmos::experimental::scope& scope) { const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); - const auto request_uri = utility::us2s(req.request_uri().to_string()); + const auto relative_uri = utility::us2s(req.relative_uri().to_string()); - if (bst::regex_search(request_uri, search_regex)) + if (bst::regex_search(relative_uri, search_regex)) { - auto path = bst::regex_replace(request_uri, search_regex, ""); + auto path = bst::regex_replace(relative_uri, search_regex, ""); if (path.size() && ('/' == path[0])) { return path.erase(0, 1); @@ -322,24 +342,41 @@ namespace nmos if (path.empty()) { - // The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission. + // "The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission." // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation - // if scope claim is presented, it has already verified eariler - if (!decoded_token.has_payload_claim(x_nmos_scope_claim) && !decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + // "Presence of an x-nmos-* claim matching an NMOS API grants implicit read only access to some API base paths as specified in Resource Servers. + // The value of the claim is a JSON object, indicating access permissions for the API.An omitted x-nmos-* object indicates that no access is permitted + // to the namespace-identified API beyond what may be granted by the presence of a matching scope." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + const auto x_nmos_scope_claim_found = decoded_token.has_payload_claim(x_nmos_scope_claim); + const auto scope_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto is_read_request = is_read_method(req.method()); + + if (is_read_request) { - // missing both x-nmos private claim and scope claim - throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); + if (!x_nmos_scope_claim_found && !scope_found) + { + // missing both x-nmos private claim and scope claim + throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); + } + } + else + { + // invalid request method + throw insufficient_scope_exception("this is not a read request, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); } } else { - // The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens. + // "The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens." // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + // "The value of each x-nmos-* claim is the access permissions object for the given user for that specific API." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#the-access-permissions-object if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) { - throw insufficient_scope_exception("claim x-nmos-" + utility::us2s(scope.name) + " " + utility::us2s(req.request_uri().to_string()) + " not accessible"); + throw insufficient_scope_exception("fail to verify claim " + x_nmos_scope_claim + ", " + utility::us2s(req.request_uri().to_string()) + " not accessible"); } } } @@ -427,6 +464,21 @@ namespace nmos return ss.str(); } + static bool is_write_method(const web::http::method& method) + { + return ((web::http::methods::POST == method) || + (web::http::methods::PUT == method) || + (web::http::methods::PATCH == method) || + (web::http::methods::DEL == method)); + }; + + static bool is_read_method (const web::http::method& method) + { + return ((web::http::methods::OPTIONS == method) || + (web::http::methods::GET == method) || + (web::http::methods::HEAD == method)); + }; + private: std::vector> validators; token_validator token_validation; From 3e6e08ad63651024ef107101a7f129018146d381 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 12 Oct 2023 18:40:44 +0100 Subject: [PATCH 085/121] Add JWT validation unit tests --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/test/jwt_validation_test.cpp | 363 ++++++++++++++++++ 2 files changed, 364 insertions(+) create mode 100644 Development/nmos/test/jwt_validation_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index b5f708fe4..c6154f748 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -46,6 +46,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/did_sdid_test.cpp nmos/test/event_type_test.cpp nmos/test/json_validator_test.cpp + nmos/test/jwt_validation_test.cpp nmos/test/paging_utils_test.cpp nmos/test/query_api_test.cpp nmos/test/sdp_utils_test.cpp diff --git a/Development/nmos/test/jwt_validation_test.cpp b/Development/nmos/test/jwt_validation_test.cpp new file mode 100644 index 000000000..692043975 --- /dev/null +++ b/Development/nmos/test/jwt_validation_test.cpp @@ -0,0 +1,363 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/jwt_validator.h" + +#include +#include +#include "bst/test/test.h" +#include "cpprest/basic_utils.h" // for utility::us2s, utility::s2us +#include "cpprest/json_utils.h" +#include "cpprest/json_validator.h" +#include "nmos/is10_versions.h" +#include "nmos/is10_schemas/is10_schemas.h" +#include "nmos/jwk_utils.h" +#include "nmos/scope.h" + +namespace +{ + using web::json::value_of; + + // this is the private key (rsa.mocks.testsuite.nmos.tv.key.pem) from the nmos-testing + // https://github.com/AMWA-TV/nmos-testing/blob/master/test_data/BCP00301/ca/intermediate/private/rsa.mocks.testsuite.nmos.tv.key.pem + const auto test_private_key = utility::s2us(R"(-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6F36+0gzW9XURoRGzRfFmIZQnCQJS7+sQrUmhPjm+X/gsNAm +zcGlgpG37jr4YbabBotlpOyRmpYfsts/9Ts1UFdqGx/aaONAmldc16arEYwSLNFW +KQ18rac0qGKAiUn2swpDIje8tTKHBE7XVLd5XVtcWrhGqX9liUFSKCb+TBuraMne +na5gA3ruXZZcn+wdHM777QQq6D1Hf9g62stePPzUeNPX+Ttulr+ju8OvQy6MK2Ij +6IHNzPT7McHUE0Z7sk7czWG60kCthRqv16OwuzR3Bhn5blPwRGXjbJJfkN6a7ck+ +4bt3LetDw/1idPArF2ONFgMO21ZJp1qm7ZnbcQIDAQABAoIBAQCKdW2XC7emsixx +9GHn1ZFlSCuCTqrHWyf++8g/Fb0z0DIHyZBFrGy996xcpQDZ4KBRbwCbHGfKcEfl +IGXk72nePKg6D2nqc/dLwGDPEz3+D7PIxtgLUEEJjIeBCmjC5bK9jpDgM8wbQEdZ +ls0Sat1Ddqv6VrGsUAAloCmfSVTf7b402o2JpJMlcqjGpZKMVZbSKlTavi6wDm9B +9BMTEER6EypK+B+jtp2CK4Jw8Tnwx7RkVVS731N0vmMqInQCzMhvO1wyeDWZkN3u +7Pzan7xiL9cL0K6icnsgHWsyC9Vq8vxMkQWuqnJ2t8u8FZik4OjD0K4uvtRuSQxU +/Exf0qPRAoGBAPPrKCtb/EMNvUdV2e3d82NsZmaQnOgoqkrpjPol8I/2YJeb7Imi +bx1Ky2qA9opydnm8br3WKGNBJGlPLhenlekaVcIOC+897ijqvy0x7ZhvUVam7N97 +Q9iYbI5iArPlDixBUnNDfsQh8ZRJy5NQz2Z6pNILToQMx83A0tKO4WbNAoGBAPPg +WlqUJNfEIuu67DHo7iFulV04CiL43LVGA0QsZiw75qJQvlsusj6pJbUN29rtihp/ +F6F09rbHqvEUv8MSp2SywqNpYgcDZxlcxnwj5RQONhkoChXYSGM6FnTVIHxa0Wai +C5JOHwIwy8mHn+roQLIe9g8vkSDKQnLrUYwj7981AoGBAOyKuOrLiqhwM4VxUSUn +H7fkUK3YQgG2Jeb99LRFhLPnpyZ/lHSo7H6IoRnItM3wUMqfnPlGLOaMLsZdfgJ8 +h5mF63KD8rjw4vwVIo6uo443LbcNrBrRzCrJLkUp8RsJ36O1OUMESnPjwwYeRmi3 +blogR6RWSK8wQbdb7lc5LodlAoGAMuQidrxrY8s+Lkr3dwLQjpFxAd7r3phoFjvh ++pv5RknJux12W7jG4WSSxdF6i5j+NMFIwRyTT1kjRuO5kI+X9t+G1mrrVeNT5GsD +0Gv9Jc5BY8aDNEPJ90rr3L2M5eZdxDkUiRdcSSy9mfR/XpnQxlrHpiua8WjDrQ+G +GOR27fECgYAQkxp8abfj4q57nWHt4Nmr5WDXrCIPNNBQvd596DGOFiSp7IsyPuzt +rKZp5TDgbxdcDIN0Jag78tzY5Ms6SHXpNe648tJBmnSHCFrx8dL95sdGKf4/DtOv +LWWWvv8Ld9XO7GPVLVFgg9wCgkIF9lUgjfhzoalCA1i1L90jcy8WDQ== +-----END RSA PRIVATE KEY----- +)"); + + // using openssl to extract the public key of the rsa.mocks.testsuite.nmos.tv.key.pem via private key + // $ openssl pkey -in rsa.mocks.testsuite.nmos.tv.key.pem -pubout + const auto test_public_key = utility::s2us(R"(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6F36+0gzW9XURoRGzRfF +mIZQnCQJS7+sQrUmhPjm+X/gsNAmzcGlgpG37jr4YbabBotlpOyRmpYfsts/9Ts1 +UFdqGx/aaONAmldc16arEYwSLNFWKQ18rac0qGKAiUn2swpDIje8tTKHBE7XVLd5 +XVtcWrhGqX9liUFSKCb+TBuraMnena5gA3ruXZZcn+wdHM777QQq6D1Hf9g62ste +PPzUeNPX+Ttulr+ju8OvQy6MK2Ij6IHNzPT7McHUE0Z7sk7czWG60kCthRqv16Ow +uzR3Bhn5blPwRGXjbJJfkN6a7ck+4bt3LetDw/1idPArF2ONFgMO21ZJp1qm7Znb +cQIDAQAB +-----END PUBLIC KEY----- +)"); + + const auto id = web::uri{ U("/test") }; + const auto audience = U("https://api-nmos.testsuite.nmos.tv"); + const utility::string_t key_id{ U("test_key") }; + + const auto jwk1 = nmos::experimental::details::private_key_to_jwk(test_private_key, key_id, jwk::public_key_uses::signing, jwk::algorithms::RS512); + const auto pems = value_of({ + value_of({ + { U("jwk"), jwk1 }, + { U("pem"), test_public_key } + }) + }); + + web::json::value make_schema(const char* schema) + { + return web::json::value::parse(utility::s2us(schema)); + } + + web::json::experimental::json_validator make_json_validator(const web::json::value& schema, const web::uri& id) + { + return web::json::experimental::json_validator + { + [&](const web::uri&) { return schema; }, + { id } + }; + } + + const nmos::experimental::jwt_validator jwt_validator(pems, [](const web::json::value& payload) + { + auto token_json_validator = make_json_validator(make_schema(nmos::is10_schemas::v1_0_x::token_schema), id); + token_json_validator.validate(payload, id); + }); + +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testJWK) +{ + const auto public_key = nmos::experimental::details::rsa_public_key(test_private_key); + BST_REQUIRE_EQUAL(test_public_key, public_key); + + const auto jwk2 = nmos::experimental::details::public_key_to_jwk(public_key, key_id, jwk::public_key_uses::signing, jwk::algorithms::RS512); + BST_REQUIRE_EQUAL(jwk1, jwk2); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenJSON) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "read": [ + // "*" + // ], + // "write": [ + // "*" + // ] + // } + //} + // missing iss(issuer) + const utility::string_t missing_iss_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ0ZXN0QHRlc3RzdWl0ZS5ubW9zLnR2IiwiYXVkIjpbImh0dHBzOi8vKi50ZXN0c3VpdGUubm1vcy50diIsImh0dHBzOi8vKi5sb2NhbCJdLCJleHAiOjQ4MjgyMDQ4MDAsImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.XS9JkK4mmDtjcyrTT0jEWpcssqeU-aZ7xQImL3f-V7KMlOFOQJ5YiT5kXV9Gxb7xYNSJxqn7ym1oL5kNxID_15VZmzsT2h2oVY5x3yOtRcuhLIpD1d4GzXFak5nvR9D6i_fCm5Ov19oF92l7dhMY_DT6HDm89maGJ9DKVxuP1jqVcwFDcXZnGak0MJYETN8nM4xIuRTmmS7W2NpzVKyfw1sCjie2QyptlPoX_KyLaiv2VMkZh-d4Pi9nA9XLjOz-Gyj0-s-NiPx9Qbocpa-eJSqzxz6gtfx8rbSNaqeGV3ehVkGC-0DJq0iIhwxpxp98qYlodz1df8gSDo106OGI_w"); + // missing sub(subject) + const utility::string_t missing_sub_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsImF1ZCI6WyJodHRwczovLyoudGVzdHN1aXRlLm5tb3MudHYiLCJodHRwczovLyoubG9jYWwiXSwiZXhwIjo0ODI4MjA0ODAwLCJpYXQiOjE2OTY4NjgyNzIsInNjb3BlIjoicmVnaXN0cmF0aW9uIiwiY2xpZW50X2lkIjoiNDU4ZjZkMDYtNDZiMS00OWZkLWI3NzgtN2MzMDQyODg4OWM2IiwieC1ubW9zLXJlZ2lzdHJhdGlvbiI6eyJyZWFkIjpbIioiXSwid3JpdGUiOlsiKiJdfX0.opBKBVFuHXbc6VepFhELRJ7INYWd_W9SBaqfXoe2dMFvCIf0HJNiDnbBrZ9qC3xyyPGR_-Bv7taNTAk67Eirh_P6dv6kPGH-cyTn4G1xCowEiGxFT-nFHyDdV4Ym50avrU6hLRHKGRy5ke0fXXHmcmQETDZpMrQq6wyg0h-kj6KneQAfNCJyqd6-jQu5VuNPsuH54iHiKOLQITOp_WDQ_3-XDQycSdbJJMhdBBnFv-l0qWqDUZAZkkNdJvKxdyhRMB_P7PhhIZck20ylJFbrcjKyMAnUj1O82L9Mriuj23p4jWd0oUiZ9VQBiTtudrrNAON6ZlIjOrBuPWIH7FXQ8w"); + // missing aud(audience) + const utility::string_t missing_aud_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJleHAiOjQ4MjgyMDQ4MDAsImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.Gm4dmsDQOw-e6B5jtBLCKl6LJex41xlMPXaeeKUoZAFj9JUMsv-CpiMIGs9RYvfpPTMJcvrGSJfAHeIkPuUmuYzBkOsD0NFrXqnWg_TmokNZo-FvJ_W3gg2pVVWG4MMTrjs_npdSU6gWBu2GslZraDTphfCo-ooiFJZgR4xPQ5EJiJYHP9m3ZQPLfgIsxX2mvIycFTjuoNuGR-T9lR70vgmfuLacDoZWreKnzSY87Ug_OWanp33kHfuCqhu6X7gTb8DwJDrpEo3Y0b8pNDms9AEDsCyxOnQGdcb4QBvcLciausFov-GLnCS_hJ1F4hpkOIj88RXQCciWpjIyaVwFMQ"); + // missing exp(expiration) + const utility::string_t missing_exp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.RQqfKCwOaBXOVH6CvPo13gT5SP8aQAUVorUoe860sSdETor6aXPZyE733OsRjMrspvgV6r6-abW4s1pUDLPcFQBPEU9QhCqGnTmACWkyBDDI2ZFfnC1tqySW7Qd1ZM8oNHNlIJUO7yXtg7YgJyWbr_Nwj-4W_cbhukIeSGBDTjG_Vhcg7O6sRZBVGFni8aqfegHMxnBFGPxfKb70C6sJbXmyb3-ufQYVs-uWbsRJmZyucjdd317lW7OTgi0nn2ZCUzI07EIArfhlJGeK4E0zzROCJbpFJs751IOpte-4lCUeHCJXg9yhS0N_jjIsdKC1G0SEMqAZ-Uo0RJ1FDU5TNg"); + + web::http::http_request request(web::http::methods::GET); + request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3")); + + // missing iss(issuer), on GET request + BST_REQUIRE_THROW(jwt_validator.validate(missing_iss_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + // missing sub(subject), on GET request + BST_REQUIRE_THROW(jwt_validator.validate(missing_sub_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + // missing aud(audience), on GET request + BST_REQUIRE_THROW(jwt_validator.validate(missing_aud_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + // missing exp(expiration), on GET request + BST_REQUIRE_THROW(jwt_validator.validate(missing_exp_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenStandardClaim) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "read": [ + // "*" + // ], + // "write": [ + // "*" + // ] + // } + //} + // invalid iat(00:00:00 1/1/2123) + const utility::string_t invalid_iat_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0Ijo0ODI4MjA0ODAwLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.pZIddJ8wtSXR4KerpmxChWiqPCIrvPj0ZsrpkrBdOvfgP_rDC-Sy7LLnP4hPEMwGqdnZKK9hJGa1uGRz2O971jwbM-n2UPzfbVpyn66A5OLnppizuWcUIij_zS0ZiXG7Lq4jmZ4vd7GnvCtwpxBKZHSXMCBwps_E7xtg6thZKoTXRIAVPu2InlNyRO5g7BmI5eLZ2vyy5WanHkL29b_lMKEzG8nOw45BdNkRq1uLB6c_aOjR1Ln1Jpcd-DIdfSGSGHLAOGg-aM0R3804W7jtNUugmZ1xyybr6g09CQst4u9A8cNdtHyob5oyCPzlGwU5fnpeYnkaKqH7mADdgC5oyA"); + // missing client_id and azp + const utility::string_t missing_clientid_azp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.PZlG03pVQMQMTCyOOSfRHQcQxLL5beDSa6J7yMPk80_KHFhUPzBttGu-cc3j6LH4tcjc_tSCbvAZTW4Po9lF4CgZ-K6DqYuCnKT3S-Q2JUSBILRVy8JcogVT12QtwNzECIIHaQsy2M4t4Geyux5lvMRPQwmfx8QOb4ZuM9_ArEDt9vWdmrJ1l81Luj6XoduwoumyivUUE7ZydFXCE1BCIPA79xOMtidPwbiym0AlSQ00lg0TsRpjcmxcy8E_BXnxKiVyRjy6R9e7eEI3ABqvnDL2KbMd4iOYPmO3Gd3r-KMTTXFx3xcQkDmfw0rAqKofp6H4S5Qhzfk-Qq90Hl6yAQ"); + // missing client_id only azp + const utility::string_t azp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImF6cCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.njx3yJsJVLO6r8P_U7Eutpr7Iygv5D0T0B9h5kPsryZ8JFc1k3OQ5-ZROeKMTl3an2VvaXXVRJrkzn_k-6W-PwbSW7XMKMNpmeXbOGGu81YFL0bLXoaZrF1Tq6_3ZTjmOj0mHV7kxIXrc239lMRPQu5fAOtLUQFVHR-IdmraWI_1kQCh1UijJSuE2wKSr31PyF2BhfQ3w17JIYWy5SHR9psygUlg9e5EgHrMOpr67gOtrsYtJ1G5enbNYQGSXN6Wcy7U35Py_foqTGk8nmExr5MnEYyUTmfisXYIfKqp6nbYyBPE_ybGUNFx8XsyTW8t_Vqa79hOzKwupx2GuqnutQ"); + // mismacthed client_id and azp + const utility::string_t mismatch_clientid_azp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsImF6cCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNyIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.hxs8NR2mMgjky0mkrhV487_stKVdbjxIkYq8kTszSdYrTqOPHZ3e9GC6husO0uirLAroD_yXngTfKekS3SDTMVbDyjNdzDQpC3eXVLQSMg5_Fi3dEHXPWRguOuQ4U6LNX6xoNQVNWWotjbHpndXzKnaySrfS7B2tzcj95pb1f64JPzvkGIWKiZ-STw1sej-T4AQpwO3whMe2_9k_ngB6r5Yvwj33nZfF5SWwiIUkQL4YW3HnSJhW2iz85kWoBrwzeSF8DboE_t2blVN16CMZPI9ZitFEFfTnAAfbx_zsV9sktLjsP2Rg659FqOpZNSo60HX4qr0GfTLPOXDhJDH9yg"); + // bad scope + const utility::string_t bad_scope_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6ImJhZCIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.udlp4ZfONuajmcIYdopQH4CUg-N1d_Ok5cibhhmG5uS3JS2dK3LyYfdra3tKsrRCGuTYvn7UCcd7XfR1fIh_4413CdQ19o5suU04e_zZRy3guDoLsyVuvY1X8_PSCkz00BgoQ1M4kjMc9bDiCzhl-2iTDxAMma34MGTz5_hCgvdPjv5SZ3k2XCQmkC-_ZI3j3WqTkvEV9XvNAUSAgF5Q-zgRJagyqdGvRBz-XMAG0aJVFEcA_X7j8eP3C5RomCPuoBDectcIysOUZGqgqzbKnJf-UjIMFiVGc2t5WntsPrLQj6OJKQgn4FRY65j0QZQFt-Pam8KIaLpmRAtKO5bRUA"); + // missing optional scope + const utility::string_t missing_scope_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.S-Jhf-q7_eNr733TZao5vHAFeYd2e9ZuLm1isF9fXyqvtodPFAQpiZJcUGZyWyeDsfbe56VWaoI0JubEBU8PFDq8IrqY3g6ySVr8jk-kB-4f9szy9-hmCjWCrZLmxJXgRR9xcYhwzgA7U_Enb5rrSO8afrOYxKxZeqySAKQryIqQYU6aOSzAGKGpkhdtZwzyraQb0LJE0nJrWonEST13Ebzg6LyXD72cISNdUN5miWn77kZ5E5fv_zb-AyvcqBAhM2FxYi6gM8L9Bv6nN-dbFxXZgiaoBkPXn--PfYb5jwiis3w3x79ZcSoUIMr3JLiWPR6U4QI2ApU4V2rEgEZzlg"); + // invalid nbf(not before 00:00:00 1/1/2123) + const utility::string_t invalid_nbf_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJuYmYiOjQ4MjgyMDQ4MDAsInNjb3BlIjoicmVnaXN0cmF0aW9uIiwiY2xpZW50X2lkIjoiNDU4ZjZkMDYtNDZiMS00OWZkLWI3NzgtN2MzMDQyODg4OWM2IiwieC1ubW9zLXJlZ2lzdHJhdGlvbiI6eyJyZWFkIjpbIioiXSwid3JpdGUiOlsiKiJdfX0.WL8XBv2IQB-2TZegIgkJ6oqjH0hkYxeAL3vL_eGE2Xy31U7RKWpq9PkmSfvf4wOe9UNkgEjfc8XIXwdQm4YB5aT8WSXkB9DnXRi6Dr8BJ2v_oRNzT8n75UAnbheqdq9CVNFSy7QVNr95oBGpSeeUL4vRCbGOghjOKUOjNjzuksoLB-52-VNoIRA0T5kwSqaRAL-r0Am8v0ucCzJ1OVtdV-WVMqw9-JrLde9oq_dJPAIZ6no4kfvE2ulxKNu8jRni4L6h3ejlrxdExiQFIt-PGHjeJ8ES8WxIYGdUNxulMiP99ta4pWkGwlRMwNvqa5saflvT1uHXKgOgENpZoZSEew"); + // expired token + const utility::string_t expired_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6MTY5Njg2OTI3MiwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.VAzXD5vkrhumbOAtqgeCJL2quZktwySI8AIE9QUOIm_wIUs7ICvd5vzWhxmgie0SGYdDvX3bQxQyFzCHU2Vccp-v3DSzO4vWx7b8zUJpIL8815dtUVpFr81V1Y8G40Ok-QRJhiJWHNHk3y-dh0AEyWtGjiqnfgMThIw3SnbVk0krUb6d-hTHHmyzk5qFkLGPVRWG2d29tTTKH0j4VY4XD_ONp-M6rTO3zGlCMV2wvlJA8jtuScRzfc5gimfNAZVPiIIqKEQHIGXX1ZaI-iJYIHxKFxXkca5K4r1p0FWaXlGgDTFQEmcgCMw9YRSv3Hl83b8ysQqkkdkkBIVugVm3ew"); + // missing private-claim (x-nmos-*) + const utility::string_t missing_private_claim_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiJ9.fyRLkLPIS1WIK5z0FCEuTss7SHAKFtsM6EUB_I7yz1YyIRR4UE-DV2V8YPCNF4dFy-4CzsCpQWUsiGvTtnfwzdcygheiyhB6QyloINJXeSCm-0wB95z285KW6AH5vbVadlBRFkMphsxkDeWP1X7lunkqv3oKei1jFGSNnc3QE4gORoGj4YqAVvUA82X3nV8eI5vNE2XHmBG_HkgTjX_JEqVr-9UcQ1EnqVDPuzrCFaQiFirZCpwg0cRHhVrmJCOrfG-bPIcX3KRfWKCaH5O2n736AwOMFqX7f4VdSbSSx7HcO1CxsmVGwQ-i8fab1IBi4KOvRsSHp3Ti3FxQTEnsEw"); + // valid token (expired at 00:00:00 1/1/2123) + const utility::string_t valid_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.ybx4VU2E6tuFbWFbCUwKyKm_MPmAXZv70x_2eyuS_Z4qF8rgB0M_yXIJMt_5padA-NPRTd8XIvnq7TLJTYMUV9-F45oQLBBWgiBQh2shsmjYg-1fHCHLxXXdlVLzxennbE38Sm60Jo-u3ZC9yFiYBMaOL5ai6f8bhzNdYaz0xbI8XZaki1pICKgVfpq1XKbXBhUD0quRwfl4PjzKfu0rtAxYc_5IxDWkxJx7BYSHR_lkMaOINda8mkSnim9V7wqkGylOc6b38OoXORtfGJCdmhc_oR9n2jwj_42r4HPo6rEul9_yYUwcYOBG65RLEB3-cbwbj8DNPguHu_TnbzBJsA"); + + { + web::http::http_request request(web::http::methods::GET); + request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3")); + + // invalid iat(issued at, greater than the current UTC time), on GET request + BST_REQUIRE_THROW(jwt_validator.validate(invalid_iat_token, request, nmos::experimental::scopes::registration, audience), std::invalid_argument); + // missing client_id and azp, on GET request + BST_REQUIRE_THROW(jwt_validator.validate(missing_clientid_azp_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + // mismacthed client_id and azp, on GET request + BST_REQUIRE_THROW(jwt_validator.validate(mismatch_clientid_azp_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + // bad scope, on GET request + BST_REQUIRE_THROW(jwt_validator.validate(bad_scope_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + // invalid nbf(not before 00:00:00 1/1/2123), on GET request + BST_REQUIRE_THROW(jwt_validator.validate(invalid_nbf_token, request, nmos::experimental::scopes::registration, audience), std::invalid_argument); + // expired token, on GET request + BST_REQUIRE_THROW(jwt_validator.validate(expired_token, request, nmos::experimental::scopes::registration, audience), std::invalid_argument); + // invalid audience, on GET request + BST_REQUIRE_THROW(jwt_validator.validate(valid_token, request, nmos::experimental::scopes::registration, U("https://api-nmos.bad_audience.com")), nmos::experimental::insufficient_scope_exception); + + // missing optional scope, on GET request + BST_REQUIRE_NO_THROW(jwt_validator.validate(missing_scope_token, request, nmos::experimental::scopes::registration, audience)); + // missing x-nmos-*, on GET request + BST_REQUIRE_NO_THROW(jwt_validator.validate(missing_private_claim_token, request, nmos::experimental::scopes::registration, audience)); + // valid token (expired at 00:00:00 1/1/2123), on GET request + BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token, request, nmos::experimental::scopes::registration, audience)); + } + + { + web::http::http_request request(web::http::methods::POST); + request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3/")); + + // missing optional scope, on POST request + BST_REQUIRE_THROW(jwt_validator.validate(missing_scope_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + // missing x-nmos-*, on POST request + BST_REQUIRE_THROW(jwt_validator.validate(missing_private_claim_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenPrivateClaim1) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "read": [ + // "*" + // ] + // } + //} + + // readonly token + const utility::string_t readonly_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl19fQ.0offeC5TooP73p2VedN27DeyHdjXIY-RFZzf2NCsyrB03dX89v2i3eHDF3nl-ZNFviNAlTiEMZqA9Sb6kvUI4jsmwpHRQ19nA9QQBKmYCog_uLvxUcGroxTJ7f9Nj8WIaWM1NZ25ZlylyOtz7QHhmkqNSVr8-eXYx8zVUtOurFUXNTN7UnCZ3ZpKoj9sR5O4bRb-11oxEKoOjQadHq22CN9_8AReKl1e3dx5aILYG1Xf_gvYxWpTfzYcgIVYjxKarE7msCUe6PnXBzJMlpu1Abu2llNQz7eCTAbNNA-PPN5cYFYuEdXSIcd8erkXSAK_8VbyizJRU1hE0uFFx0r3Iw"); + + web::http::http_request request(web::http::methods::POST); + request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc")); + + // test x-nmos-* + // valid token with x-nmos-registration read only set + BST_REQUIRE_THROW(jwt_validator.validate(readonly_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenPrivateClaim2) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "write": [ + // "*" + // ] + // } + //} + + // valid token + // "x-nmos-registration" : { + // "write": [ + // "*" + // ] + // } + const utility::string_t valid_token1 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiKiJdfX0.exiZrwWY1nvxnS_LA0R0YMbzkpQNbzUneKO5ruwkSlqW7XdI3TQgRoiGXW1vDbC8XH4RQCD8SPoS2vuX4rLMfLGZGLtpHUFp3khAvs142Oc6K15ldYGfGpjeyDxSw9syRtl37XiG1MPOygYaqjEOXpI9Ljwj8jzGyJXpLGWzLHPnC9SkNCfe7C1ATjz86938qEW-ksxKP7CCQbNVWy13Trti7ow5jiSSd71rqB448tliNi9CDcd_xlx9SvRXZmvomUQOWhJlAQnwKbT7krk1gWqw2JFtOVblP8sKsQHdLX6wxc6F_pHlwJJmWg-cLs0oOV7PKzokIqw7wHN0fnQLtQ"); + // + // valid token + // "x-nmos-registration" : { + // "write": [ + // "health/*" + // ] + // } + const utility::string_t valid_token2 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoLyoiXX19.l-dZnLsODuEyGjpT9tWDq5GpzNFtjAIhLDWZI20yfskzKpW0dagNLFp3sKfZOAZspMp3DLb-lCRIp1fXS9rlkBB6mQ-z3XMf4pJXPFaCkxf3EGEsTtpsoYw6jic9Ue8EYPAx7Ma1ersd6TH41HZDi06K9Ko0vwl7qQ4HzctEXMA53afCkc4vIlChWZ8bFAU6gF2avfzU5nAsLPAGrGATFPG4meCmPFtdjnZBLPwyINOP9rCN3Qw6Hwt5f9Y7obAcbuwK9adTYFDqti9j3hzg8p-AGE4Ixo_ItOw0Kg1D1TowlPm7U2pMz-7S4OmwEq8alktufhLPuX_M3m_W5-37Ew"); + // + // valid token + // "x-nmos-registration" : { + // "write": [ + // "health/nodes/*" + // ] + // } + const utility::string_t valid_token3 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoL25vZGVzLyoiXX19.UGcQLz47PYhVFulnKgVoFb6V53bvIwjACPHyvm8P8NCkYcnMvjcyDKPCBnIfEoVT8a9LbCK2rFisgo4Rw3NXhDnfbGRoq-4Dad8TbvFpJfEs-Wcb1GDKeaCuS78NvEW8KhbTXoOD04Yj6vRkLSg_Vk-nalNmpjG1vnUPuLO2DZux36l7Ggaq3kDBcIfDCIicrA7V2cu9qL9EqzgEB2DXtrjZ0y219nkGp7UK6wxdI8_-p1LqvpU7vNJmqserri_waEJ-vWhP3JU8b5aeFuQS946Sjr3PHAAraO0RkDAje20dGPpCE5doMmjNZRIEa529MO-g3LQoZABhUCIr57Z0kA"); + // + // bad token + // "x-nmos-registration" : { + // "write": [ + // "bad/*" + // ] + // } + const utility::string_t invalid_token1 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiYmFkLyoiXX19.nZZ3gmmuvEJhF51EJM3OMwsT9_xDwix0U2y8tGQ6G-3nrIMDjM5zAYk8_IeyOXgI063wcQLwY0s83hYZXjKH4ifEb9xDAGBSaF-lijVQAaAbzTX5aIFEngz6pBloUGpWnS7LUJbDDhX8bBO00dH8Umh88GNaxxfBmKTDBb7CAlRpMjRHVid4MPdDAcO0SkeI8K5_71LitDjoXGXkqd1r_AKFh5jRQvdZuNy-6pkg1xSHS8HRsskNIguIYFEpciw22KMDbVZKSBiWUq1tTjGzwv2fDrEEnQZDvyNHqep6DxOOzrJPQtwZoADcq1simZ6IZFKf0ewo6SMMfOmC7JNcuQ"); + // + // bad token + // "x-nmos-registration" : { + // "write": [ + // "health/bad/*" + // ] + // } + const utility::string_t invalid_token2 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoL2JhZC8qIl19fQ.o_5XAUKjv4Dyf7cxvuL6bP8GsFhV5IcscndUYenzmGo50sRw0sHvi7eANMTdoh1HAMvTcAYzdpPRPEsIrk2tvKsEVKQzKjCVXw_uKc_Xew00qEF6nUbCPAPd0TotJXTQKtqP_NIcUsRDFWL4X9wpAJQkPdv9xzE_j3RKmbOv3uQq3iRA-TBSOcgJlsCZ37IGNM-_gyOzyRZSKaaY2xAHuPpEt7Gm88sjRmgerIyRLC9zSFt-5jIYAOXlUSMv1tsQK0BQCvqxF_nppHKyfpQacxDTN-UOiD7DvJWhMTpny0mM0mwFnoS-UyQq_cHPA03BDF9-noYeBqo4VMRMx_gnlA"); + + web::http::http_request request(web::http::methods::POST); + request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc")); + + BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token1, request, nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token2, request, nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token3, request, nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_THROW(jwt_validator.validate(invalid_token1, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(jwt_validator.validate(invalid_token2, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); +} From 7ab8cd72cbc436efabaddf005fcb61c194393fd8 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 22 Nov 2023 23:50:00 +0000 Subject: [PATCH 086/121] Add access token validation callback, and tidy up comments --- Development/nmos-cpp-node/main.cpp | 4 +- Development/nmos-cpp-registry/main.cpp | 4 +- Development/nmos/api_utils.cpp | 16 ++-- Development/nmos/api_utils.h | 3 +- Development/nmos/authorization.cpp | 96 ++++++-------------- Development/nmos/authorization.h | 32 +------ Development/nmos/authorization_behaviour.cpp | 4 +- Development/nmos/authorization_handlers.cpp | 91 ++++++++++++++++++- Development/nmos/authorization_handlers.h | 34 ++++++- Development/nmos/authorization_operation.cpp | 14 +-- Development/nmos/events_ws_api.cpp | 20 ++-- Development/nmos/jwt_validator_impl.cpp | 2 +- Development/nmos/ws_api_utils.cpp | 21 +++-- Development/nmos/ws_api_utils.h | 4 +- 14 files changed, 198 insertions(+), 147 deletions(-) diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index a6b7998de..81ae55209 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -121,8 +121,8 @@ int main(int argc, char* argv[]) if (nmos::experimental::fields::server_authorization(node_model.settings)) { node_implementation - .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(node_model, authorization_state, gate)) - .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(node_model, authorization_state, gate)); + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(node_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(node_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)); } if (nmos::experimental::fields::client_authorization(node_model.settings)) { diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index f2a5d50c0..01a1f9bcf 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -117,8 +117,8 @@ int main(int argc, char* argv[]) if (nmos::experimental::fields::server_authorization(registry_model.settings)) { registry_implementation - .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(registry_model, authorization_state, gate)) - .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(registry_model, authorization_state, gate)); + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(registry_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(registry_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)); } // Set up the registry server diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 6e2f0fbc3..e26aae440 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -764,11 +764,11 @@ namespace nmos { // JWT validation to confirm authentication credentials and an access token that allows access to the protected resource // see https://tools.ietf.org/html/rfc6750#section-3 - web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, slog::base_gate& gate_) + web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, validate_authorization_token_handler access_token_validation, slog::base_gate& gate_) { using namespace web::http::experimental::listener::api_router_using_declarations; - return [&model, &authorization_state, scope, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + return [&model, &authorization_state, scope, access_token_validation, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); @@ -776,12 +776,8 @@ namespace nmos web::uri token_issuer; const auto audience = with_read_lock(model.mutex, [&] { return nmos::get_host_name(model.settings); }); - auto error = with_read_lock(authorization_state.mutex, [&authorization_state, &audience, req, &scope, &token_issuer, &gate_] - { - // note: the validate_authorization will update the token_issuer, i.e. using with_write_lock to protected it - return nmos::experimental::validate_authorization(authorization_state.issuers, req, scope, audience, token_issuer, gate_); - }); - + // note: the validate_authorization returns the token_issuer via function parameter + const auto error = nmos::experimental::validate_authorization(req, scope, audience, token_issuer, access_token_validation, gate_); if (error) { // set error repsonse @@ -790,8 +786,8 @@ namespace nmos const auto retry_after = with_read_lock(model.mutex, [&] { return nmos::experimental::fields::service_unavailable_retry_after(model.settings); }); set_error_reply(res, realm, retry_after, error); - // if error was deal to no matching keys, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) - if (error.value == nmos::experimental::authorization_error::no_matching_keys) + // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) + if (error.value == authorization_error::no_matching_keys) { slog::log(gate, SLOG_FLF) << "Authorization warning: " << error.message; diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 54e8d1a47..19f0444f7 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -8,6 +8,7 @@ #include "cpprest/http_listener.h" // for web::http::experimental::listener::http_listener_config #include "cpprest/regex_utils.h" #include "cpprest/ws_listener.h" // for web::websockets::experimental::listener::websocket_listener_config +#include "nmos/authorization_handlers.h" // for nmos::experimental::validate_authorization_token_handler #include "nmos/settings.h" // just a forward declaration of nmos::settings namespace slog @@ -210,7 +211,7 @@ namespace nmos { // JWT validation to confirm authentication credentials and an access token that allows access to the protected resource // see https://tools.ietf.org/html/rfc6750#section-3 - web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, slog::base_gate& gate); + web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); // set error response void set_error_reply(web::http::http_response& res, const utility::string_t& realm, int retry_after, const nmos::experimental::authorization_error& error); diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp index bd33764ca..004e8c9d0 100644 --- a/Development/nmos/authorization.cpp +++ b/Development/nmos/authorization.cpp @@ -14,7 +14,7 @@ namespace nmos without_authentication_exception(const std::string& message) : std::runtime_error(message) {} }; - bool is_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate) + bool is_access_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate) { if (access_token.empty()) { @@ -40,7 +40,7 @@ namespace nmos } catch (const std::exception& e) { - slog::log(gate, SLOG_FLF) << "test token expiry: " << e.what(); + slog::log(gate, SLOG_FLF) << "Test token expiry error: " << e.what(); } // reaching here, token validation has failed, treat it as expired @@ -69,86 +69,50 @@ namespace nmos } catch (const std::exception& e) { - slog::log(gate, SLOG_FLF) << "failed to get client_id from header: " << e.what(); + slog::log(gate, SLOG_FLF) << "Failed to get client_id from header: " << e.what(); } return{}; } - authorization_error validate_authorization(const utility::string_t& access_token, const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate) + namespace details { - if (access_token.empty()) - { - slog::log(gate, SLOG_FLF) << "missing access token"; - return{ authorization_error::without_authentication, "missing access token" }; - } - - try - { - // extract the token issuer from the token - token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); - } - catch (const std::exception& e) - { -#if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << "unable to extract token issuer from access token: " << e.what(); -#else - slog::log(gate, SLOG_FLF) << "unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token; -#endif - return{ authorization_error::failed, e.what() }; - } - - // find the relevent issuer's public keys to validate the token - std::string error; - auto issuer = issuers.find(token_issuer); - if(issuers.end() != issuer) + authorization_error validate_authorization(const utility::string_t& access_token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) { - slog::log(gate, SLOG_FLF) << "validate access token against " << utility::us2s(issuer->first.to_string()) << " public keys"; + if (access_token.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing access token"; + return{ authorization_error::without_authentication, "Missing access token" }; + } try { - // if jwt_validator has not already set up, treat it as no public keys to validate token - if (issuer->second.jwt_validator.is_initialized()) - { - issuer->second.jwt_validator.validate(access_token, request, scope, audience); - return{ authorization_error::succeeded }; - } - else - { - slog::log(gate, SLOG_FLF) << "no " << utility::us2s(issuer->first.to_string()) << " public keys to validate access token"; - } + // extract the token issuer from the token + token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); } - catch (const no_matching_keys_exception& e) + catch (const std::exception& e) { - error = e.what(); #if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what(); #else - slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token; #endif + return{ authorization_error::failed, e.what() }; } - catch (const insufficient_scope_exception& e) + + if (access_token_validation) { - // validator can decode the token, but insufficient scope -#if !defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; -#endif - return{ authorization_error::insufficient_scope, e.what() }; + return access_token_validation(access_token, request, scope, audience); } - catch (const std::exception& e) + else { - // validator can decode the token, with general failure -#if !defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; -#endif - return{ authorization_error::failed, e.what() }; + std::string error{ "Access token validation callback is not set up to validate the access token" }; + slog::log(gate, SLOG_FLF) << error; + return{ authorization_error::failed, error }; } } - - // reach here must be because there are no public keys to validate token - return{ authorization_error::no_matching_keys, error }; } - authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate) + authorization_error validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) { try { @@ -168,7 +132,7 @@ namespace nmos } const auto access_token = token.substr(scheme.length()); - return validate_authorization(access_token, issuers, request, scope, audience, token_issuer, gate); + return details::validate_authorization(access_token, request, scope, audience, token_issuer, access_token_validation, gate); } catch (const without_authentication_exception& e) { @@ -180,9 +144,9 @@ namespace nmos // Clients SHOULD use the "Authorization Request Header Field" method. // Clients MAY use "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 - authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate) + authorization_error ws_validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) { - auto error = validate_authorization(issuers, request, scope, audience, token_issuer, gate); + auto error = validate_authorization(request, scope, audience, token_issuer, access_token_validation, gate); if (error) { @@ -193,10 +157,10 @@ namespace nmos if (!query.empty()) { auto querys = web::uri::split_query(query); - auto it = querys.find(U("access_token")); - if (querys.end() != it) + auto found = querys.find(U("access_token")); + if (querys.end() != found) { - error = nmos::experimental::validate_authorization(it->second, issuers, request, scope, audience, token_issuer, gate); + error = details::validate_authorization(found->second, request, scope, audience, token_issuer, access_token_validation, gate); } } } diff --git a/Development/nmos/authorization.h b/Development/nmos/authorization.h index 6e35b57ba..9a331f01d 100644 --- a/Development/nmos/authorization.h +++ b/Development/nmos/authorization.h @@ -1,9 +1,7 @@ #ifndef NMOS_AUTHORIZATION_H #define NMOS_AUTHORIZATION_H -#include -#include "cpprest/basic_utils.h" -#include "cpprest/json_utils.h" +#include "nmos/authorization_handlers.h" // for nmos::experimental::validate_authorization_token_handler, nmos::experimental::authorization_error, and nmos::experimental::scope #include "nmos/issuers.h" namespace web @@ -26,39 +24,17 @@ namespace nmos { namespace experimental { - struct scope; - - struct authorization_error - { - enum status_t - { - succeeded, - without_authentication, // failure: access protected resource request without authentication - insufficient_scope, // failure: access protected resource request higher privileges - no_matching_keys, // failure: no matching keys for the token validation - failed // failure: access protected resource request with authentication but failed - }; - - authorization_error() : value(without_authentication) {} - authorization_error(status_t value, const std::string& message = {}) : value(value), message(message) {} - - status_t value; - std::string message; - - operator bool() const { return succeeded != value; } - }; - - bool is_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate); + bool is_access_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate); utility::string_t get_client_id(const web::http::http_headers& headers, slog::base_gate& gate); - authorization_error validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate); + authorization_error validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket // Clients SHOULD use the "Authorization Request Header Field" method. // Clients MAY use "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 - authorization_error ws_validate_authorization(const issuers& issuers, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, slog::base_gate& gate); + authorization_error ws_validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); } } diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp index 6d45e71df..a1a0526aa 100644 --- a/Development/nmos/authorization_behaviour.cpp +++ b/Development/nmos/authorization_behaviour.cpp @@ -141,7 +141,7 @@ namespace nmos // is it not a scopeless client (where scopeless client doesn't access any protected APIs, i.e. doesn't require to register to Authorization server) if (with_read_lock(model.mutex, [&] { return details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)).size(); })) { - // is the client already registered to Authorization server, i.e. found in cache + // is the client already registered to Authorization server, i.e. found it in cache if (!client_metadata.is_null()) { auto is_authorization_code_flow_next = [&] @@ -149,7 +149,7 @@ namespace nmos auto lock = authorization_state.read_lock(); const auto& bearer_token = authorization_state.bearer_token; - return (!bearer_token.is_valid_access_token() || is_token_expired(bearer_token.access_token(), authorization_state.issuers, authorization_state.authorization_server_uri, gate)); + return (!bearer_token.is_valid_access_token() || is_access_token_expired(bearer_token.access_token(), authorization_state.issuers, authorization_state.authorization_server_uri, gate)); }; auto is_client_expired = [&] diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index da498459b..2d2ccd3dd 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -201,8 +201,8 @@ namespace nmos #endif std::ignore = system(browser_cmd.c_str()); - // TODO: process Authorization Server error response - // notify authorization_code_flow in the authorization_behaviour thread + // hmm, notify authorization_code_flow in the authorization_behaviour thread + // in the event of user cancels the authorization code flow process }; } @@ -253,16 +253,97 @@ namespace nmos }; } - validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, slog::base_gate& gate) + // construct callback to validate OAuth 2.0 authorization access token + validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) { - return[&](const nmos::experimental::scope& scope) + return[&](const utility::string_t& access_token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) + { + //web::uri token_issuer; + + try + { + // extract the token issuer from the token + auto token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); + + auto lock = authorization_state.read_lock(); + + std::string error; + auto issuer = authorization_state.issuers.find(token_issuer); + if (authorization_state.issuers.end() != issuer) + { + slog::log(gate, SLOG_FLF) << "Validate access token against " << utility::us2s(issuer->first.to_string()) << " public keys"; + + try + { + // if jwt_validator has not already set up, treat it as no public keys to validate token + if (issuer->second.jwt_validator.is_initialized()) + { + issuer->second.jwt_validator.validate(access_token, request, scope, audience); + return authorization_error{ authorization_error::succeeded }; + } + else + { + std::stringstream ss; + ss << "No public keys from " << utility::us2s(issuer->first.to_string()) << " to validate access token"; + error = ss.str(); + slog::log(gate, SLOG_FLF) << error; + } + } + catch (const no_matching_keys_exception& e) + { + error = e.what(); +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; +#else + slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; +#endif + } + catch (const insufficient_scope_exception& e) + { + // validator can decode the token, but insufficient scope +#if !defined (NDEBUG) + slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::insufficient_scope, e.what() }; + } + catch (const std::exception& e) + { + // validator can decode the token, with general failure +#if !defined (NDEBUG) + slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + } + + // reaching here, must be no public keys to validate token + return authorization_error{ authorization_error::no_matching_keys, error }; + } + catch (const std::exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + + }; + } + + // construct callback to validate OAuth 2.0 authorization + validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) + { + return[&, access_token_validation](const nmos::experimental::scope& scope) { slog::log(gate, SLOG_FLF) << "Make authorization validation"; - return nmos::experimental::details::make_validate_authorization_handler(model, authorization_state, scope, gate); + return nmos::experimental::details::make_validate_authorization_handler(model, authorization_state, scope, access_token_validation, gate); }; } + // construct callback to retrieve OAuth 2.0 authorization bearer token authorization_token_handler make_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) { return[&]() diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h index cdbde092d..709a2ff52 100644 --- a/Development/nmos/authorization_handlers.h +++ b/Development/nmos/authorization_handlers.h @@ -30,6 +30,26 @@ namespace nmos { struct authorization_state; + struct authorization_error + { + enum status_t + { + succeeded, + without_authentication, // failure: access protected resource request without authentication + insufficient_scope, // failure: access protected resource request higher privileges + no_matching_keys, // failure: no matching keys for the token validation + failed // failure: access protected resource request with authentication but failed + }; + + authorization_error() : value(without_authentication) {} + authorization_error(status_t value, const std::string& message = {}) : value(value), message(message) {} + + status_t value; + std::string message; + + operator bool() const { return succeeded != value; } + }; + namespace fields { // authorization_server_uri: the uri of the authorization server, where the client is registered @@ -123,17 +143,23 @@ namespace nmos authorization_config_handler make_authorization_config_handler(const web::json::value& authorization_server_metadata, const web::json::value& client_metadata, slog::base_gate& gate); authorization_config_handler make_authorization_config_handler(const authorization_state& authorization_state, slog::base_gate& gate); + // callback to validate OAuth 2.0 authorization access token + // this callback should not throw exceptions + typedef std::function validate_authorization_token_handler; + // construct callback to validate OAuth 2.0 authorization access token + validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate); + // callback to return the OAuth 2.0 validation route handler // this callback is executed at the beginning while walking the supported API routes typedef std::function validate_authorization_handler; - // construct callback to validate authorization - validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, slog::base_gate& gate); + // construct callback to validate OAuth 2.0 authorization + validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); // callback to return OAuth 2.0 authorization bearer token // this callback is execute while create http_client - typedef std::function authorization_token_handler; - // construct callback to retrieve authorization bearer token // this callback should not throw exceptions + typedef std::function authorization_token_handler; + // construct callback to retrieve OAuth 2.0 authorization bearer token authorization_token_handler make_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate); } } diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 139dbc3ad..6fcf06f28 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -1637,19 +1637,19 @@ namespace nmos { // authorization code workflow timeout authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "authorization code workflow timeout"; + slog::log(gate, SLOG_FLF) << "Authorization code workflow timeout"; } else if (nmos::experimental::authorization_state::access_token_received == authorization_flow) { // access token received access_token_received = true; - slog::log(gate, SLOG_FLF) << "access token received"; + slog::log(gate, SLOG_FLF) << "Acess token received"; } else { // authorization code workflow failure authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "authorization code workflow failure"; + slog::log(gate, SLOG_FLF) << "Authorization code workflow failure"; } } else @@ -1663,13 +1663,13 @@ namespace nmos { // access token received access_token_received = true; - slog::log(gate, SLOG_FLF) << "access token received"; + slog::log(gate, SLOG_FLF) << "Access token received"; } else { // authorization code workflow failure authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "authorization code workflow failure"; + slog::log(gate, SLOG_FLF) << "Authorization code workflow failure"; } } } @@ -1677,7 +1677,7 @@ namespace nmos { // no handler to start the authorization code grant workflow authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "no authorization code workflow handler"; + slog::log(gate, SLOG_FLF) << "No authorization code workflow handler"; } model.notify(); @@ -1900,7 +1900,7 @@ namespace nmos { try { - const auto pem = jwk_to_public_key(jwk); // can throw jwk_exception + const auto& pem = jwk_to_public_key(jwk); // can throw jwk_exception web::json::push_back(pems, web::json::value_of({ { U("jwk"), jwk }, diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index ab0b54a43..9507db4d8 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -62,9 +62,9 @@ namespace nmos }; } - web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::authorization_state& authorization_state, slog::base_gate& gate_) + web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::validate_authorization_token_handler access_token_validation, slog::base_gate& gate_) { - return [&model, &authorization_state, &gate_](web::http::http_request req) + return [&model, &authorization_state, access_token_validation, &gate_](web::http::http_request req) { nmos::ws_api_gate gate(gate_, req.request_uri()); auto lock = model.write_lock(); @@ -79,9 +79,8 @@ namespace nmos const auto& settings = model.settings; web::uri token_issuer; - - authorization_state.write_lock(); - const auto error = nmos::experimental::ws_validate_authorization(authorization_state.issuers, req, nmos::experimental::scopes::events, nmos::get_host_name(settings), token_issuer, gate_); + // note: the ws_validate_authorization returns the token_issuer via function parameter + const auto error = nmos::experimental::ws_validate_authorization(req, nmos::experimental::scopes::events, nmos::get_host_name(settings), token_issuer, access_token_validation, gate_); if (error) { // set error repsonse @@ -92,12 +91,17 @@ namespace nmos nmos::experimental::details::set_error_reply(res, realm, retry_after, error); req.reply(res); - // if error was deal to no matching keys, trigger authorization_token_issuer_thread to fetch public keys from the token issuer + // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) if (error.value == nmos::experimental::authorization_error::no_matching_keys) { slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << error.message; - authorization_state.fetch_token_issuer_pubkeys = true; - authorization_state.token_issuer = token_issuer; + + with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] + { + authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; + }); + model.notify(); } else diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index 1ad8d26a4..7fcb57843 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -26,7 +26,7 @@ namespace nmos if (pubkeys.is_array()) { - // empty out all jwt validators + // empty out all jwt verifiers validators.clear(); // create jwt verifier for each public key diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp index 1c0e446a9..b66defcb3 100644 --- a/Development/nmos/ws_api_utils.cpp +++ b/Development/nmos/ws_api_utils.cpp @@ -12,19 +12,17 @@ namespace nmos namespace experimental { // callbacks from this function are called with the model locked, and may read or write directly to the model - ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, slog::base_gate& gate) + ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) { - return [&model, &authorization_state, &gate](web::http::http_request& request, const nmos::experimental::scope& scope) + return [&model, &authorization_state, access_token_validation, &gate](web::http::http_request& request, const nmos::experimental::scope& scope) { if (web::http::methods::OPTIONS != request.method() && nmos::experimental::fields::server_authorization(model.settings)) { const auto& settings = model.settings; web::uri token_issuer; - - authorization_state.write_lock(); - - const auto error = ws_validate_authorization(authorization_state.issuers, request, scope, nmos::get_host_name(settings), token_issuer, gate); + // note: the ws_validate_authorization returns the token_issuer via function parameter + const auto error = ws_validate_authorization(request, scope, nmos::get_host_name(settings), token_issuer, access_token_validation, gate); if (error) { // set error repsonse @@ -35,12 +33,17 @@ namespace nmos nmos::experimental::details::set_error_reply(res, realm, retry_after, error); request.reply(res); - // if error was deal to no matching keys, trigger authorization_token_issuer_thread to fetch public keys from the token issuer + // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) if (error.value == authorization_error::no_matching_keys) { slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << error.message; - authorization_state.fetch_token_issuer_pubkeys = true; - authorization_state.token_issuer = token_issuer; + + with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] + { + authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; + }); + model.notify(); } else diff --git a/Development/nmos/ws_api_utils.h b/Development/nmos/ws_api_utils.h index b45e56e44..fdf341a02 100644 --- a/Development/nmos/ws_api_utils.h +++ b/Development/nmos/ws_api_utils.h @@ -3,7 +3,7 @@ #include #include "cpprest/http_msg.h" -#include "nmos/scope.h" +#include "nmos/authorization_handlers.h" namespace slog { @@ -19,7 +19,7 @@ namespace nmos struct authorization_state; typedef std::function ws_validate_authorization_handler; - ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, slog::base_gate& gate); + ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); } } From 7842a56b9044c6074914fd6a489a97e1f405749a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 23 Nov 2023 11:45:35 +0000 Subject: [PATCH 087/121] Use const reference bearer token for the token fetch task --- Development/nmos/authorization_operation.cpp | 2 +- Development/nmos/authorization_redirect_api.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 6fcf06f28..2a800a040 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -967,7 +967,7 @@ namespace nmos throw web::http::oauth2::experimental::oauth2_exception(U("Unsupported grant: ") + token_state.grant_type.name); } - }).then([=, &authorization_state, &token_state, &gate](web::http::oauth2::experimental::oauth2_token bearer_token) + }).then([=, &authorization_state, &token_state, &gate](const web::http::oauth2::experimental::oauth2_token& bearer_token) { token_state.bearer_token = bearer_token; diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp index 09e342e6f..a969f0dab 100644 --- a/Development/nmos/authorization_redirect_api.cpp +++ b/Development/nmos/authorization_redirect_api.cpp @@ -276,7 +276,7 @@ namespace nmos // exchange authorization code for bearer token request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, client_secret, scope, redirect_uri, state, code_verifier, token_endpoint_auth_meth, client_assertion, gate); - auto request = request_token.then([&model, &authorization_state, &scope, &gate](web::http::oauth2::experimental::oauth2_token bearer_token) + auto request = request_token.then([&model, &authorization_state, &scope, &gate](const web::http::oauth2::experimental::oauth2_token& bearer_token) { auto lock = authorization_state.write_lock(); From a8a382a9da1ccf76e11f4186e2898a10607285f0 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 23 Nov 2023 18:06:38 +0000 Subject: [PATCH 088/121] Tidy up --- Development/nmos/authorization_behaviour.cpp | 18 ++++++++++++------ Development/nmos/authorization_handlers.cpp | 2 -- Development/nmos/authorization_operation.cpp | 9 ++------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp index a1a0526aa..1d59c4b53 100644 --- a/Development/nmos/authorization_behaviour.cpp +++ b/Development/nmos/authorization_behaviour.cpp @@ -144,7 +144,8 @@ namespace nmos // is the client already registered to Authorization server, i.e. found it in cache if (!client_metadata.is_null()) { - auto is_authorization_code_flow_next = [&] + // no token or token expired + auto is_access_token_bad = [&] { auto lock = authorization_state.read_lock(); @@ -193,7 +194,8 @@ namespace nmos // fetch client metadata from Authorization server in case it has been changed (e.g. changed by the system admin) if (details::request_client_metadata_from_openid_connect(model, authorization_state, load_ca_certificates, save_authorization_client, gate)) { - mode = (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : (is_authorization_code_flow_next() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch); + mode = (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation // client credentials flow + : (is_access_token_bad() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch); // bad access token must start from authorization code flow, otherise do token refresh } else { @@ -209,14 +211,18 @@ namespace nmos // no registration_access_token and registration_client_uri found, treat it has connected with a non-OpenID Connect server // start grant flow based on what been defined in the settings // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration - mode = is_client_expired() ? client_registration : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : (is_authorization_code_flow_next() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); + mode = is_client_expired() ? client_registration // client registration + : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation // client credentials flow + : (is_access_token_bad() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); // bad access token must start from authorization code flow, otherise do token refresh } } else { // start grant flow based on what been defined in the settings // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration - mode = is_client_expired() ? client_registration : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : (is_authorization_code_flow_next() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); + mode = is_client_expired() ? client_registration // client registration + : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation // client credentials flow + : (is_access_token_bad() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); // bad access token must start from authorization code flow, otherise do token refresh } } else @@ -276,7 +282,7 @@ namespace nmos details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, false, gate); // reaching here, there must be failure within the authorization operation, - // start authorization sequence again on next available Authorization server + // start the authorization sequence again on next available Authorization server authorization_service_error = true; mode = request_authorization_server_metadata; break; @@ -287,7 +293,7 @@ namespace nmos details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, true, gate); // reaching here, there must be failure within the authorization operation, - // start authorization sequence again on next available Authorization server + // start the authorization sequence again on next available Authorization server authorization_service_error = true; mode = request_authorization_server_metadata; break; diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 2d2ccd3dd..97bfc067c 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -258,8 +258,6 @@ namespace nmos { return[&](const utility::string_t& access_token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) { - //web::uri token_issuer; - try { // extract the token issuer from the token diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 2a800a040..fc101b342 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -1718,14 +1718,9 @@ namespace nmos }; auto bearer_token_requests(pplx::task_from_result()); - web::http::oauth2::experimental::oauth2_token bearer_token; - std::set scopes; const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); - nmos::with_read_lock(authorization_state.mutex, [&] - { - bearer_token = authorization_state.bearer_token.is_valid_access_token() ? authorization_state.bearer_token : web::http::oauth2::experimental::oauth2_token{}; - scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); - }); + const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + const auto bearer_token = nmos::with_read_lock(authorization_state.mutex, [&] { return authorization_state.bearer_token.is_valid_access_token() ? authorization_state.bearer_token : web::http::oauth2::experimental::oauth2_token{}; }); token_shared_state token_state( grant, bearer_token, From b6e4abb2e3142ba9a5a1740f97170b3a9f8e107d Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 30 Nov 2023 17:02:04 +0000 Subject: [PATCH 089/121] Split the access token validation to basic validation (validate using issuer public keys) and registered claims validation. Basic validation can be provided via the validate_authorization_token_handler callback. --- Development/nmos/api_utils.cpp | 12 +- Development/nmos/authorization.cpp | 47 +- Development/nmos/authorization_handlers.cpp | 60 +- Development/nmos/authorization_handlers.h | 6 +- Development/nmos/events_ws_api.cpp | 12 +- Development/nmos/jwt_validator.h | 24 +- Development/nmos/jwt_validator_impl.cpp | 521 +++++++++--------- Development/nmos/test/jwt_validation_test.cpp | 64 +-- 8 files changed, 384 insertions(+), 362 deletions(-) diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index e26aae440..50351beb8 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -777,19 +777,19 @@ namespace nmos web::uri token_issuer; const auto audience = with_read_lock(model.mutex, [&] { return nmos::get_host_name(model.settings); }); // note: the validate_authorization returns the token_issuer via function parameter - const auto error = nmos::experimental::validate_authorization(req, scope, audience, token_issuer, access_token_validation, gate_); - if (error) + const auto result = nmos::experimental::validate_authorization(req, scope, audience, token_issuer, access_token_validation, gate_); + if (!result) { // set error repsonse auto realm = web::http::get_host_port(req).first; if (realm.empty()) { realm = with_read_lock(model.mutex, [&] { return nmos::get_host(model.settings); }); } const auto retry_after = with_read_lock(model.mutex, [&] { return nmos::experimental::fields::service_unavailable_retry_after(model.settings); }); - set_error_reply(res, realm, retry_after, error); + set_error_reply(res, realm, retry_after, result); // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) - if (error.value == authorization_error::no_matching_keys) + if (result.value == authorization_error::no_matching_keys) { - slog::log(gate, SLOG_FLF) << "Authorization warning: " << error.message; + slog::log(gate, SLOG_FLF) << "Authorization warning: " << result.message; with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] { @@ -802,7 +802,7 @@ namespace nmos } else { - slog::log(gate, SLOG_FLF) << "Authorization error: " << error.message; + slog::log(gate, SLOG_FLF) << "Authorization error: " << result.message; } throw nmos::details::to_api_finally_handler{}; // in order to skip other route handlers and then send the response diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp index 004e8c9d0..46bd68c99 100644 --- a/Development/nmos/authorization.cpp +++ b/Development/nmos/authorization.cpp @@ -33,7 +33,7 @@ namespace nmos const auto& issuer = issuers.find(token_issuer); if (issuers.end() != issuer) { - issuer->second.jwt_validator.validate_expiry(access_token); + issuer->second.jwt_validator.basic_validation(access_token); return false; } } @@ -101,7 +101,40 @@ namespace nmos if (access_token_validation) { - return access_token_validation(access_token, request, scope, audience); + try + { + // do basic access token token validation + const auto result = access_token_validation(access_token); + + if (result) + { + // do AMWA IS-10 registered claims validation + nmos::experimental::jwt_validator::registered_claims_validation(access_token, request.method(), request.relative_uri(), scope, audience); + + return authorization_error{ authorization_error::succeeded }; + } + return result; + } + catch (const insufficient_scope_exception& e) + { + // validator can decode the token, but insufficient scope +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Insufficient scope error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Insufficient scope error: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::insufficient_scope, e.what() }; + } + catch (const std::exception& e) + { + // validator can decode the token, with general failure +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } } else { @@ -146,11 +179,11 @@ namespace nmos // See https://tools.ietf.org/html/rfc6750#section-2 authorization_error ws_validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) { - auto error = validate_authorization(request, scope, audience, token_issuer, access_token_validation, gate); + auto result = validate_authorization(request, scope, audience, token_issuer, access_token_validation, gate); - if (error) + if (!result) { - error = { authorization_error::without_authentication, "missing access token" }; + result = { authorization_error::without_authentication, "missing access token" }; // test "URI Query Parameter" const auto& query = request.request_uri().query(); @@ -160,11 +193,11 @@ namespace nmos auto found = querys.find(U("access_token")); if (querys.end() != found) { - error = details::validate_authorization(found->second, request, scope, audience, token_issuer, access_token_validation, gate); + result = details::validate_authorization(found->second, request, scope, audience, token_issuer, access_token_validation, gate); } } } - return error; + return result; } } } diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 97bfc067c..085bdd9da 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -1,5 +1,6 @@ #include "nmos/authorization_handlers.h" +#include #include "cpprest/basic_utils.h" #include "cpprest/json_validator.h" #include "cpprest/response_type.h" @@ -256,12 +257,12 @@ namespace nmos // construct callback to validate OAuth 2.0 authorization access token validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) { - return[&](const utility::string_t& access_token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) + return[&](const utility::string_t& access_token) { try { // extract the token issuer from the token - auto token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); + const auto token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); auto lock = authorization_state.read_lock(); @@ -276,46 +277,68 @@ namespace nmos // if jwt_validator has not already set up, treat it as no public keys to validate token if (issuer->second.jwt_validator.is_initialized()) { - issuer->second.jwt_validator.validate(access_token, request, scope, audience); + // do access token basic validation, including token schema validation and token issuer public keys validation + issuer->second.jwt_validator.basic_validation(access_token); + return authorization_error{ authorization_error::succeeded }; } else { std::stringstream ss; - ss << "No public keys from " << utility::us2s(issuer->first.to_string()) << " to validate access token"; + ss << "No " << utility::us2s(issuer->first.to_string()) << " public keys to validate access token"; error = ss.str(); slog::log(gate, SLOG_FLF) << error; + + return authorization_error{ authorization_error::no_matching_keys, error }; } } - catch (const no_matching_keys_exception& e) + catch (const web::json::json_exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + catch (const jwt::error::token_verification_exception& e) { - error = e.what(); #if defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys"; + slog::log(gate, SLOG_FLF) << "Token verification error: " << e.what(); #else - slog::log(gate, SLOG_FLF) << e.what() << " against " << utility::us2s(issuer->first.to_string()) << " public keys; access_token: " << access_token; + slog::log(gate, SLOG_FLF) << "Token verification error: " << e.what() << "; access_token: " << access_token; #endif + return authorization_error{ authorization_error::failed, e.what() }; } - catch (const insufficient_scope_exception& e) + catch (const no_matching_keys_exception& e) { - // validator can decode the token, but insufficient scope -#if !defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "No matching public keys error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "No matching public keys error: " << e.what() << "; access_token: " << access_token; #endif - return authorization_error{ authorization_error::insufficient_scope, e.what() }; + return authorization_error{ authorization_error::no_matching_keys, e.what() }; } catch (const std::exception& e) { - // validator can decode the token, with general failure -#if !defined (NDEBUG) - slog::log(gate, SLOG_FLF) << e.what() << "; access_token: " << access_token; +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what() << "; access_token: " << access_token; #endif return authorization_error{ authorization_error::failed, e.what() }; } } + else + { + std::stringstream ss; + ss << "No " << utility::us2s(token_issuer.to_string()) << " public keys to validate access token"; + error = ss.str(); + slog::log(gate, SLOG_FLF) << error; - // reaching here, must be no public keys to validate token - return authorization_error{ authorization_error::no_matching_keys, error }; + // no public keys to validate token + return authorization_error{ authorization_error::no_matching_keys, error }; + } } catch (const std::exception& e) { @@ -326,7 +349,6 @@ namespace nmos #endif return authorization_error{ authorization_error::failed, e.what() }; } - }; } diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h index 709a2ff52..20c215807 100644 --- a/Development/nmos/authorization_handlers.h +++ b/Development/nmos/authorization_handlers.h @@ -2,8 +2,8 @@ #define NMOS_AUTHORIZATION_HANDLERS_H #include +#include #include "cpprest/api_router.h" -#include "cpprest/oauth2.h" #include "nmos/scope.h" #include "nmos/settings.h" @@ -47,7 +47,7 @@ namespace nmos status_t value; std::string message; - operator bool() const { return succeeded != value; } + operator bool() const { return succeeded == value; } }; namespace fields @@ -145,7 +145,7 @@ namespace nmos // callback to validate OAuth 2.0 authorization access token // this callback should not throw exceptions - typedef std::function validate_authorization_token_handler; + typedef std::function validate_authorization_token_handler; // construct callback to validate OAuth 2.0 authorization access token validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate); diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index 9507db4d8..cda94fa4d 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -80,21 +80,21 @@ namespace nmos web::uri token_issuer; // note: the ws_validate_authorization returns the token_issuer via function parameter - const auto error = nmos::experimental::ws_validate_authorization(req, nmos::experimental::scopes::events, nmos::get_host_name(settings), token_issuer, access_token_validation, gate_); - if (error) + const auto result = nmos::experimental::ws_validate_authorization(req, nmos::experimental::scopes::events, nmos::get_host_name(settings), token_issuer, access_token_validation, gate_); + if (!result) { // set error repsonse auto realm = web::http::get_host_port(req).first; if (realm.empty()) { realm = nmos::get_host(settings); } web::http::http_response res; const auto retry_after = nmos::experimental::fields::service_unavailable_retry_after(settings); - nmos::experimental::details::set_error_reply(res, realm, retry_after, error); + nmos::experimental::details::set_error_reply(res, realm, retry_after, result); req.reply(res); // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) - if (error.value == nmos::experimental::authorization_error::no_matching_keys) + if (result.value == nmos::experimental::authorization_error::no_matching_keys) { - slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << error.message; + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << result.message; with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] { @@ -106,7 +106,7 @@ namespace nmos } else { - slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << error.message; + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << req.request_uri().path() << ": " << result.message; } return false; } diff --git a/Development/nmos/jwt_validator.h b/Development/nmos/jwt_validator.h index 331bcc626..1cea62f47 100644 --- a/Development/nmos/jwt_validator.h +++ b/Development/nmos/jwt_validator.h @@ -1,6 +1,7 @@ #ifndef NMOS_JWT_VALIDATOR_H #define NMOS_JWT_VALIDATOR_H +#include #include "cpprest/base_uri.h" namespace web @@ -40,20 +41,35 @@ namespace nmos } // callback for JSON validating access token - typedef std::function token_validator; + typedef std::function token_json_validator; class jwt_validator { public: jwt_validator() {} - jwt_validator(const web::json::value& pub_keys, token_validator token_validation); + jwt_validator(const web::json::value& pub_keys, token_json_validator token_validation); + // is JWT validator initialised bool is_initialized() const; - void validate_expiry(const utility::string_t& token) const; - void validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) const; + // Token JSON validation + // may throw + void json_validation(const utility::string_t& token) const; + // Basic token validation, including token schema validation and token issuer public keys validation + // may throw + void basic_validation(const utility::string_t& token) const; + + // Registered claims validation + // may throw + static void registered_claims_validation(const utility::string_t& token, const web::http::method& method, const web::uri& relative_uri, const scope& scope, const utility::string_t& audience); + + // Get token client Id + // may throw static utility::string_t get_client_id(const utility::string_t& token); + + // Get token issuer + // may throw static web::uri get_token_issuer(const utility::string_t& token); private: diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index 7fcb57843..cc09d630d 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -1,12 +1,11 @@ #include "nmos/jwt_validator.h" #include +#include #include "cpprest/basic_utils.h" -#include "cpprest/http_msg.h" #include "cpprest/json.h" #include "cpprest/regex_utils.h" #include "cpprest/uri_schemes.h" -#include "jwt-cpp/traits/nlohmann-json/traits.h" #include "nmos/authorization_utils.h" #include "nmos/json_fields.h" @@ -19,7 +18,7 @@ namespace nmos class jwt_validator_impl { public: - explicit jwt_validator_impl(const web::json::value& pubkeys, token_validator token_validation) + explicit jwt_validator_impl(const web::json::value& pubkeys, token_json_validator token_validation) : token_validation(token_validation) { using namespace jwt::traits; @@ -73,352 +72,317 @@ namespace nmos } } - void validate_expiry(const utility::string_t& token) const + // Basic token validation + // may throw + void basic_validation(const utility::string_t& token) const { using namespace jwt::traits; - // verify JWT is well formed - auto decoded_token = jwt::decode(utility::us2s(token)); + const auto decoded_token = jwt::decode(utility::us2s(token)); + + // do token JSON validation + if (token_validation) { token_validation(web::json::value::parse(utility::s2us(decoded_token.get_payload()))); } + else { throw web::json::json_exception("No JOSN token valiation callback to validate access token"); } + + std::vector errors; + + // is JWT validator set up + if (0 == validators.size()) { errors.push_back("no JWT validator to perform access token validation"); } + // do basic token validation for (const auto& validator : validators) { try { - // verify the signature & some common claims, such as exp, iat, nbf etc + // verify the signature & some of the common claims, such as exp, iat, nbf etc validator.verify(decoded_token); - // token not expired + // basic token validation successfully return; } - catch (const jwt::error::signature_verification_exception&) + catch (const jwt::error::signature_verification_exception& e) { // ignore, try next validator + errors.push_back(e.what()); } } - // no public keys to validate access token - throw std::runtime_error("no public keys to validate access token expiry"); + // reaching here, there must be because no matching public key to validate the access token + + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + + const auto token_issuer = web::uri{ utility::s2us(decoded_token.get_issuer()) }; + // no matching public keys for the token, re-fetch public keys from token issuer + throw no_matching_keys_exception(token_issuer, format_errors(errors)); } - void validate(const utility::string_t& token, const web::http::http_request& req, const scope& scope, const utility::string_t& audience) const + // Registered claims validation + // may throw + static void registered_claims_validation(const utility::string_t& token, const web::http::method& method, const web::uri& relative_uri_, const scope& scope, const utility::string_t& audience) { using namespace jwt::traits; - // verify JWT is well formed - auto decoded_token = jwt::decode(utility::us2s(token)); + const auto decoded_token = jwt::decode(utility::us2s(token)); - // do bearer token payload JSON validation - if (token_validation) - { - token_validation(web::json::value::parse(utility::s2us(decoded_token.get_payload()))); - } + // verify Registered Claims - std::vector errors; + // iss (Identifies principal that issued the JWT) + // The "iss" value is a case-sensitive string containing a StringOrURI value. + // see https://tools.ietf.org/html/rfc7519#section-4.1.1 + // iss is not needed to validate as this token may be coming from an alternative Authorization server, which would have a different iss then the current in used Authorization server. - if (0 == validators.size()) - { - // no JWT validator to perform access token validation - errors.push_back("no JWT validator to perform access token validation"); - } + // sub (Identifies the subject of the JWT) + // hmm, not sure how to verify sub as it could be anything + // see https://tools.ietf.org/html/rfc7519#section-4.1.2 - const auto validate_scope = !scope.name.empty(); + // aud (Identifies the recipients of the JWT) + // This claim MUST be a JSON array containing the fully resolved domain names of the intended recipients, or a domain name containing + // wild - card characters in order to target a subset of devices on a network. Such wild-carding of domain names is documented in RFC 4592. + // If aud claim does not match the fully resolved domain name of the resource server, the Resource Server MUST reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#aud + // see https://tools.ietf.org/html/rfc7519#section-4.1.3 - for (const auto& validator : validators) + auto verify_aud = [&decoded_token](const utility::string_t& audience_) { - try + auto strip_trailing_dot = [](const std::string& audience_) { - // verify the signature & some common claims, such as exp, iat, nbf etc - validator.verify(decoded_token); - - // common claims verified (i.e. validator/public key successfully verify the token's signature), - // from this point onwards any error detected will be treated as failure + auto audience = audience_; + if (!audience.empty() && U('.') == audience.back()) + { + audience.pop_back(); + } + return audience; + }; - // verify Registered Claims + auto audience = strip_trailing_dot(utility::us2s(audience_)); + std::vector segments; + boost::split(segments, audience, boost::is_any_of(".")); - // iss (Identifies principal that issued the JWT) - // The "iss" value is a case-sensitive string containing a StringOrURI value. - // see https://tools.ietf.org/html/rfc7519#section-4.1.1 - // iss is not needed to validate as this token may be coming from an alternative Authorization server, which would have a different iss then the current in used Authorization server. + const auto& auds = decoded_token.get_audience(); + for (const auto& aud_ : auds) + { + // strip the scheme (https://) if presented + auto aud = strip_trailing_dot(aud_); + web::http::uri aud_uri(utility::s2us(aud)); + if (!aud_uri.scheme().empty()) + { + aud = utility::us2s(aud_uri.host()); + } - // sub (Identifies the subject of the JWT) - // hmm, not sure how to verify sub as it could be anything - // see https://tools.ietf.org/html/rfc7519#section-4.1.2 + // is the audience an exact match to the token audience + if (audience == aud) + { + return true; + } - // aud (Identifies the recipients of the JWT) - // This claim MUST be a JSON array containing the fully resolved domain names of the intended recipients, or a domain name containing - // wild - card characters in order to target a subset of devices on a network. Such wild-carding of domain names is documented in RFC 4592. - // If aud claim does not match the fully resolved domain name of the resource server, the Resource Server MUST reject the token. - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#aud - // see https://tools.ietf.org/html/rfc7519#section-4.1.3 + // do reverse segment matching between audience and token audience + std::vector aud_segments; + boost::split(aud_segments, aud, boost::is_any_of(".")); - auto verify_aud = [&decoded_token](const utility::string_t& audience_) + if (segments.size() >= aud_segments.size() && aud_segments.size()) { - auto strip_trailing_dot = [](const std::string& audience_) + // token audience got to be in wildcard domain name format, leftmost is a "*" charcater + // if not it is not going to match + // see https://tools.ietf.org/html/rfc4592#section-2.1.1 + if (aud_segments[0] != "*") { - auto audience = audience_; - if (!audience.empty() && U('.') == audience.back()) - { - audience.pop_back(); - } - return audience; - }; - - auto audience = strip_trailing_dot(utility::us2s(audience_)); - std::vector segments; - boost::split(segments, audience, boost::is_any_of(".")); + return false; + } - const auto& auds = decoded_token.get_audience(); - for (const auto& aud_ : auds) + // token audience is in wildcard domain name format + // let's do a segment to segment comparison between audience and token audience + bool matched{ true }; + auto idx = aud_segments.size() - 1; + for (auto it = aud_segments.rbegin(); it != aud_segments.rend() && matched; ++it) { - // strip the scheme (https://) if presented - auto aud = strip_trailing_dot(aud_); - web::http::uri aud_uri(utility::s2us(aud)); - if (!aud_uri.scheme().empty()) - { - aud = utility::us2s(aud_uri.host()); - } - - // is the audience an exact match to the token audience - if (audience == aud) + if (idx && *it != segments[idx--]) { - return true; - } - - // do reverse segment matching between audience and token audience - std::vector aud_segments; - boost::split(aud_segments, aud, boost::is_any_of(".")); - - if (segments.size() >= aud_segments.size() && aud_segments.size()) - { - // token audience got to be in wildcard domain name format, leftmost is a "*" charcater - // if not it is not going to match - // see https://tools.ietf.org/html/rfc4592#section-2.1.1 - if (aud_segments[0] != "*") - { - return false; - } - - // token audience is in wildcard domain name format - // let's do a segment to segment comparison between audience and token audience - bool matched{ true }; - auto idx = aud_segments.size() - 1; - for (auto it = aud_segments.rbegin(); it != aud_segments.rend() && matched; ++it) - { - if (idx && *it != segments[idx--]) - { - matched = false; - } - } - if (matched) - { - return true; - } + matched = false; } } - return false; - }; - if (!verify_aud(audience)) - { - throw insufficient_scope_exception(utility::us2s(audience) + " not found in audience"); - } - - // scope optional - // If scope claim does not contain the expected scope, the Resource Server reject the token. - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope - auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) - { - if (decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + if (matched) { - const auto& scope_claim = decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::scope)); - const auto scopes_set = scopes(utility::s2us(scope_claim.as_string())); - return (scopes_set.end() != std::find(scopes_set.begin(), scopes_set.end(), scope)); + return true; } - return true; - }; - if (!verify_scope(scope)) - { - throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); } + } + return false; + }; + if (!verify_aud(audience)) + { + throw insufficient_scope_exception(utility::us2s(audience) + " not found in audience"); + } - // verify client_id and azp (optional) - auto verify_client_id = [&decoded_token]() - { - const auto client_id_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::client_id)); - const auto azp_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::azp)); + // scope optional + // If scope claim does not contain the expected scope, the Resource Server reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope + auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) + { + if (decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + { + const auto& scope_claim = decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto scopes_set = scopes(utility::s2us(scope_claim.as_string())); + return (scopes_set.end() != std::find(scopes_set.begin(), scopes_set.end(), scope)); + } + return true; + }; + if (!verify_scope(scope)) + { + throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); + } - if ((client_id_found && !azp_found) || (!client_id_found && azp_found)) - { - return true; - } + // verify client_id and azp (optional) + auto verify_client_id = [&decoded_token]() + { + const auto client_id_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::client_id)); + const auto azp_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::azp)); - if (client_id_found && - azp_found && - decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::client_id)).as_string() == decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::azp)).as_string()) - { - return true; - } + if ((client_id_found && !azp_found) || (!client_id_found && azp_found)) + { + return true; + } - return false; - }; - if (!verify_client_id()) - { - throw insufficient_scope_exception("missing client_id or azp, or client_id and azp are not matching"); - } + if (client_id_found && + azp_found && + decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::client_id)).as_string() == decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::azp)).as_string()) + { + return true; + } - // verify Private Claims + return false; + }; + if (!verify_client_id()) + { + throw insufficient_scope_exception("missing client_id or azp, or client_id and azp are not matching"); + } - // x-nmos-* (Contains information particular to the NMOS API the token is intended for) - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- - auto verify_x_nmos_scope_claim = [&decoded_token, req](const std::string& x_nmos_scope_claim_, const std::string& path) - { - if (!decoded_token.has_payload_claim(x_nmos_scope_claim_)) { return false; } - const auto x_nmos_scope_claim = decoded_token.get_payload_claim(x_nmos_scope_claim_).to_json(); + // verify Private Claims + + // x-nmos-* (Contains information particular to the NMOS API the token is intended for) + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + auto verify_x_nmos_scope_claim = [&decoded_token, &method](const std::string& x_nmos_scope_claim_, const std::string& path) + { + if (!decoded_token.has_payload_claim(x_nmos_scope_claim_)) { return false; } + const auto x_nmos_scope_claim = decoded_token.get_payload_claim(x_nmos_scope_claim_).to_json(); - if (!x_nmos_scope_claim.is_null()) + if (!x_nmos_scope_claim.is_null()) + { + auto accessible = [&x_nmos_scope_claim, &method, &path](const std::string& access_right) + { + if (x_nmos_scope_claim.contains(access_right)) { - auto accessible = [&x_nmos_scope_claim, req, &path](const std::string& access_right) + auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); + for (auto& accessible_path : accessible_paths) { - if (x_nmos_scope_claim.contains(access_right)) + // construct path regex for regex comparison + + auto acc_path = accessible_path.get(); + // replace any '*' => '.*' + boost::replace_all(acc_path, "*", ".*"); + const bst::regex path_regex(acc_path); + if (bst::regex_match(path, path_regex)) { - auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); - for (auto& accessible_path : accessible_paths) - { - // construct path regex for regex comparison - - auto acc_path = accessible_path.get(); - // replace any '*' => '.*' - boost::replace_all(acc_path, "*", ".*"); - const bst::regex path_regex(acc_path); - if (bst::regex_match(path, path_regex)) - { - return true; - } - } + return true; } - return false; - }; - - // write accessible - if (is_write_method(req.method())) - { - return accessible("write"); - } - - // read accessible - if (is_read_method(req.method())) - { - return accessible("read"); } } return false; }; - // verify the relevant x-nmos-* private claim - if (validate_scope) + // write accessible + if (is_write_method(method)) { - const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); + return accessible("write"); + } - // extract {path} from /x-nmos/{api name, the scope name}/{api version}/{path} - auto extract_path = [req](const nmos::experimental::scope& scope) - { - const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); - const auto relative_uri = utility::us2s(req.relative_uri().to_string()); + // read accessible + if (is_read_method(method)) + { + return accessible("read"); + } + } + return false; + }; - if (bst::regex_search(relative_uri, search_regex)) - { - auto path = bst::regex_replace(relative_uri, search_regex, ""); - if (path.size() && ('/' == path[0])) - { - return path.erase(0, 1); - } - else - { - return std::string{}; - } - } - return std::string{};; - }; - const auto path = extract_path(scope); + // verify the relevant x-nmos-* private claim + if (!scope.name.empty()) + { + const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); + const auto relative_uri = utility::us2s(relative_uri_.to_string()); + // extract {path} from /x-nmos/{api name, the scope name}/{api version}/{path} + auto extract_path = [&relative_uri](const nmos::experimental::scope& scope) + { + const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); - if (path.empty()) + if (bst::regex_search(relative_uri, search_regex)) + { + auto path = bst::regex_replace(relative_uri, search_regex, ""); + if (path.size() && ('/' == path[0])) { - // "The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission." - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation - - // "Presence of an x-nmos-* claim matching an NMOS API grants implicit read only access to some API base paths as specified in Resource Servers. - // The value of the claim is a JSON object, indicating access permissions for the API.An omitted x-nmos-* object indicates that no access is permitted - // to the namespace-identified API beyond what may be granted by the presence of a matching scope." - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- - const auto x_nmos_scope_claim_found = decoded_token.has_payload_claim(x_nmos_scope_claim); - const auto scope_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope)); - const auto is_read_request = is_read_method(req.method()); - - if (is_read_request) - { - if (!x_nmos_scope_claim_found && !scope_found) - { - // missing both x-nmos private claim and scope claim - throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); - } - } - else - { - // invalid request method - throw insufficient_scope_exception("this is not a read request, " + utility::us2s(req.request_uri().to_string()) + " not accessible"); - } + return path.erase(0, 1); } else { - // "The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens." - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation - - // "The value of each x-nmos-* claim is the access permissions object for the given user for that specific API." - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#the-access-permissions-object - if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) - { - throw insufficient_scope_exception("fail to verify claim " + x_nmos_scope_claim + ", " + utility::us2s(req.request_uri().to_string()) + " not accessible"); - } + return std::string{}; } } + return std::string{};; + }; + const auto path = extract_path(scope); - // token validate successfully - return; - } - catch (const insufficient_scope_exception&) - { - throw; - } - catch (const jwt::error::token_verification_exception& e) + if (path.empty()) { - throw std::invalid_argument(e.what()); + // "The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + + // "Presence of an x-nmos-* claim matching an NMOS API grants implicit read only access to some API base paths as specified in Resource Servers. + // The value of the claim is a JSON object, indicating access permissions for the API.An omitted x-nmos-* object indicates that no access is permitted + // to the namespace-identified API beyond what may be granted by the presence of a matching scope." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + const auto x_nmos_scope_claim_found = decoded_token.has_payload_claim(x_nmos_scope_claim); + const auto scope_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto is_read_request = is_read_method(method); + + if (is_read_request) + { + if (!x_nmos_scope_claim_found && !scope_found) + { + // missing both x-nmos private claim and scope claim + throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + relative_uri + " not accessible"); + } + } + else + { + // invalid request method + throw insufficient_scope_exception("this is not a read request, " + relative_uri + " not accessible"); + } } - catch (const jwt::error::signature_verification_exception& e) + else { - // ignore, try next validator - errors.push_back(e.what()); + // "The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + + // "The value of each x-nmos-* claim is the access permissions object for the given user for that specific API." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#the-access-permissions-object + if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) + { + throw insufficient_scope_exception("fail to verify claim " + x_nmos_scope_claim + ", " + relative_uri + " not accessible"); + } } } - - // reaching here, there must be because no matching public key to validate the access token - - // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss - // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it - // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource - // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. - // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." - // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys - - const auto token_issuer = web::uri{ utility::s2us(decoded_token.get_issuer()) }; - // no matching public keys for the token, re-fetch public keys from token issuer - throw no_matching_keys_exception(token_issuer, format_errors(errors)); } + // Get token client Id // may throw static utility::string_t get_client_id(const utility::string_t& token) { using namespace jwt::traits; - // verify JWT is well formed auto decoded_token = jwt::decode(utility::us2s(token)); // token does not guarantee to have client_id // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#client_id @@ -441,12 +405,12 @@ namespace nmos return{}; } + // Get token issuer // may throw static web::uri get_token_issuer(const utility::string_t& token) { using namespace jwt::traits; - // verify JWT is well formed auto decoded_token = jwt::decode(utility::us2s(token)); return utility::s2us(decoded_token.get_issuer()); } @@ -481,39 +445,46 @@ namespace nmos private: std::vector> validators; - token_validator token_validation; + token_json_validator token_validation; }; } - jwt_validator::jwt_validator(const web::json::value& pubkeys, token_validator token_validation) + jwt_validator::jwt_validator(const web::json::value& pubkeys, token_json_validator token_validation) : impl(new details::jwt_validator_impl(pubkeys, token_validation)) { } + // is JWT validator initialised bool jwt_validator::is_initialized() const { return impl ? true : false; } - void jwt_validator::validate_expiry(const utility::string_t& token) const + // Basic token validation + // may throw + void jwt_validator::basic_validation(const utility::string_t& token) const { if (!impl) { throw std::runtime_error("JWT validator has not initiliased"); } - impl->validate_expiry(token); + impl->basic_validation(token); } - void jwt_validator::validate(const utility::string_t& token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience) const + // Registered claims validation + // may throw + void jwt_validator::registered_claims_validation(const utility::string_t& token, const web::http::method& method, const web::uri& relative_uri, const scope& scope, const utility::string_t& audience) { - if (!impl) { throw std::runtime_error("JWT validator has not initiliased"); } - - impl->validate(token, request, scope, audience); + details::jwt_validator_impl::registered_claims_validation(token, method, relative_uri, scope, audience); } + // Get token client Id + // may throw utility::string_t jwt_validator::get_client_id(const utility::string_t& token) { return details::jwt_validator_impl::get_client_id(token); } + // Get token issuer + // may throw web::uri jwt_validator::get_token_issuer(const utility::string_t& token) { return details::jwt_validator_impl::get_token_issuer(token); diff --git a/Development/nmos/test/jwt_validation_test.cpp b/Development/nmos/test/jwt_validation_test.cpp index 692043975..163bf10cd 100644 --- a/Development/nmos/test/jwt_validation_test.cpp +++ b/Development/nmos/test/jwt_validation_test.cpp @@ -3,11 +3,11 @@ #include #include +#include #include "bst/test/test.h" #include "cpprest/basic_utils.h" // for utility::us2s, utility::s2us #include "cpprest/json_utils.h" #include "cpprest/json_validator.h" -#include "nmos/is10_versions.h" #include "nmos/is10_schemas/is10_schemas.h" #include "nmos/jwk_utils.h" #include "nmos/scope.h" @@ -143,17 +143,14 @@ BST_TEST_CASE(testAccessTokenJSON) // missing exp(expiration) const utility::string_t missing_exp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.RQqfKCwOaBXOVH6CvPo13gT5SP8aQAUVorUoe860sSdETor6aXPZyE733OsRjMrspvgV6r6-abW4s1pUDLPcFQBPEU9QhCqGnTmACWkyBDDI2ZFfnC1tqySW7Qd1ZM8oNHNlIJUO7yXtg7YgJyWbr_Nwj-4W_cbhukIeSGBDTjG_Vhcg7O6sRZBVGFni8aqfegHMxnBFGPxfKb70C6sJbXmyb3-ufQYVs-uWbsRJmZyucjdd317lW7OTgi0nn2ZCUzI07EIArfhlJGeK4E0zzROCJbpFJs751IOpte-4lCUeHCJXg9yhS0N_jjIsdKC1G0SEMqAZ-Uo0RJ1FDU5TNg"); - web::http::http_request request(web::http::methods::GET); - request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3")); - // missing iss(issuer), on GET request - BST_REQUIRE_THROW(jwt_validator.validate(missing_iss_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_iss_token), web::json::json_exception); // missing sub(subject), on GET request - BST_REQUIRE_THROW(jwt_validator.validate(missing_sub_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_sub_token), web::json::json_exception); // missing aud(audience), on GET request - BST_REQUIRE_THROW(jwt_validator.validate(missing_aud_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_aud_token), web::json::json_exception); // missing exp(expiration), on GET request - BST_REQUIRE_THROW(jwt_validator.validate(missing_exp_token, request, nmos::experimental::scopes::registration, audience), web::json::json_exception); + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_exp_token), web::json::json_exception); } //////////////////////////////////////////////////////////////////////////////////////////// @@ -208,40 +205,29 @@ BST_TEST_CASE(testAccessTokenStandardClaim) const utility::string_t valid_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.ybx4VU2E6tuFbWFbCUwKyKm_MPmAXZv70x_2eyuS_Z4qF8rgB0M_yXIJMt_5padA-NPRTd8XIvnq7TLJTYMUV9-F45oQLBBWgiBQh2shsmjYg-1fHCHLxXXdlVLzxennbE38Sm60Jo-u3ZC9yFiYBMaOL5ai6f8bhzNdYaz0xbI8XZaki1pICKgVfpq1XKbXBhUD0quRwfl4PjzKfu0rtAxYc_5IxDWkxJx7BYSHR_lkMaOINda8mkSnim9V7wqkGylOc6b38OoXORtfGJCdmhc_oR9n2jwj_42r4HPo6rEul9_yYUwcYOBG65RLEB3-cbwbj8DNPguHu_TnbzBJsA"); { - web::http::http_request request(web::http::methods::GET); - request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3")); - - // invalid iat(issued at, greater than the current UTC time), on GET request - BST_REQUIRE_THROW(jwt_validator.validate(invalid_iat_token, request, nmos::experimental::scopes::registration, audience), std::invalid_argument); - // missing client_id and azp, on GET request - BST_REQUIRE_THROW(jwt_validator.validate(missing_clientid_azp_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); - // mismacthed client_id and azp, on GET request - BST_REQUIRE_THROW(jwt_validator.validate(mismatch_clientid_azp_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); - // bad scope, on GET request - BST_REQUIRE_THROW(jwt_validator.validate(bad_scope_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); // invalid nbf(not before 00:00:00 1/1/2123), on GET request - BST_REQUIRE_THROW(jwt_validator.validate(invalid_nbf_token, request, nmos::experimental::scopes::registration, audience), std::invalid_argument); + BST_REQUIRE_THROW(jwt_validator.basic_validation(invalid_nbf_token), jwt::error::token_verification_exception); // expired token, on GET request - BST_REQUIRE_THROW(jwt_validator.validate(expired_token, request, nmos::experimental::scopes::registration, audience), std::invalid_argument); + BST_REQUIRE_THROW(jwt_validator.basic_validation(expired_token), jwt::error::token_verification_exception); // invalid audience, on GET request - BST_REQUIRE_THROW(jwt_validator.validate(valid_token, request, nmos::experimental::scopes::registration, U("https://api-nmos.bad_audience.com")), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, U("https://api-nmos.bad_audience.com")), nmos::experimental::insufficient_scope_exception); // missing optional scope, on GET request - BST_REQUIRE_NO_THROW(jwt_validator.validate(missing_scope_token, request, nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(jwt_validator.basic_validation(missing_scope_token)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_scope_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience)); // missing x-nmos-*, on GET request - BST_REQUIRE_NO_THROW(jwt_validator.validate(missing_private_claim_token, request, nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(jwt_validator.basic_validation(missing_private_claim_token)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_private_claim_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience)); // valid token (expired at 00:00:00 1/1/2123), on GET request - BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token, request, nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(jwt_validator.basic_validation(valid_token)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience)); } { - web::http::http_request request(web::http::methods::POST); - request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3/")); - // missing optional scope, on POST request - BST_REQUIRE_THROW(jwt_validator.validate(missing_scope_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_scope_token, web::http::methods::POST, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); // missing x-nmos-*, on POST request - BST_REQUIRE_THROW(jwt_validator.validate(missing_private_claim_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_private_claim_token, web::http::methods::POST, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); } } @@ -276,12 +262,9 @@ BST_TEST_CASE(testAccessTokenPrivateClaim1) // readonly token const utility::string_t readonly_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl19fQ.0offeC5TooP73p2VedN27DeyHdjXIY-RFZzf2NCsyrB03dX89v2i3eHDF3nl-ZNFviNAlTiEMZqA9Sb6kvUI4jsmwpHRQ19nA9QQBKmYCog_uLvxUcGroxTJ7f9Nj8WIaWM1NZ25ZlylyOtz7QHhmkqNSVr8-eXYx8zVUtOurFUXNTN7UnCZ3ZpKoj9sR5O4bRb-11oxEKoOjQadHq22CN9_8AReKl1e3dx5aILYG1Xf_gvYxWpTfzYcgIVYjxKarE7msCUe6PnXBzJMlpu1Abu2llNQz7eCTAbNNA-PPN5cYFYuEdXSIcd8erkXSAK_8VbyizJRU1hE0uFFx0r3Iw"); - web::http::http_request request(web::http::methods::POST); - request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc")); - // test x-nmos-* // valid token with x-nmos-registration read only set - BST_REQUIRE_THROW(jwt_validator.validate(readonly_token, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(readonly_token, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); } //////////////////////////////////////////////////////////////////////////////////////////// @@ -352,12 +335,9 @@ BST_TEST_CASE(testAccessTokenPrivateClaim2) // } const utility::string_t invalid_token2 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoL2JhZC8qIl19fQ.o_5XAUKjv4Dyf7cxvuL6bP8GsFhV5IcscndUYenzmGo50sRw0sHvi7eANMTdoh1HAMvTcAYzdpPRPEsIrk2tvKsEVKQzKjCVXw_uKc_Xew00qEF6nUbCPAPd0TotJXTQKtqP_NIcUsRDFWL4X9wpAJQkPdv9xzE_j3RKmbOv3uQq3iRA-TBSOcgJlsCZ37IGNM-_gyOzyRZSKaaY2xAHuPpEt7Gm88sjRmgerIyRLC9zSFt-5jIYAOXlUSMv1tsQK0BQCvqxF_nppHKyfpQacxDTN-UOiD7DvJWhMTpny0mM0mwFnoS-UyQq_cHPA03BDF9-noYeBqo4VMRMx_gnlA"); - web::http::http_request request(web::http::methods::POST); - request.set_request_uri(U("https://api-nmos.testsuite.nmos.tv/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc")); - - BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token1, request, nmos::experimental::scopes::registration, audience)); - BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token2, request, nmos::experimental::scopes::registration, audience)); - BST_REQUIRE_NO_THROW(jwt_validator.validate(valid_token3, request, nmos::experimental::scopes::registration, audience)); - BST_REQUIRE_THROW(jwt_validator.validate(invalid_token1, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); - BST_REQUIRE_THROW(jwt_validator.validate(invalid_token2, request, nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token1, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token2, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token3, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(invalid_token1, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(invalid_token2, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); } From d463b98a1ba69d72a864d7ecf7a3003faefff135 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 30 Nov 2023 19:05:53 +0000 Subject: [PATCH 090/121] Correct ws_validate_authorization usage after previous modification --- Development/nmos/ws_api_utils.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp index b66defcb3..27429d734 100644 --- a/Development/nmos/ws_api_utils.cpp +++ b/Development/nmos/ws_api_utils.cpp @@ -22,21 +22,21 @@ namespace nmos web::uri token_issuer; // note: the ws_validate_authorization returns the token_issuer via function parameter - const auto error = ws_validate_authorization(request, scope, nmos::get_host_name(settings), token_issuer, access_token_validation, gate); - if (error) + const auto result = ws_validate_authorization(request, scope, nmos::get_host_name(settings), token_issuer, access_token_validation, gate); + if (!result) { // set error repsonse auto realm = web::http::get_host_port(request).first; if (realm.empty()) { realm = nmos::get_host(settings); } web::http::http_response res; const auto retry_after = nmos::experimental::fields::service_unavailable_retry_after(settings); - nmos::experimental::details::set_error_reply(res, realm, retry_after, error); + nmos::experimental::details::set_error_reply(res, realm, retry_after, result); request.reply(res); // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) - if (error.value == authorization_error::no_matching_keys) + if (result.value == authorization_error::no_matching_keys) { - slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << error.message; + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << result.message; with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] { @@ -48,7 +48,7 @@ namespace nmos } else { - slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << error.message; + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << result.message; } return false; } From 40ef839a27b2bbba8150e5557f4d531ed4be4924 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 30 Nov 2023 19:08:05 +0000 Subject: [PATCH 091/121] Up JWT_VERSION_CUR to 0.6.0 --- Development/cmake/NmosCppDependencies.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/cmake/NmosCppDependencies.cmake b/Development/cmake/NmosCppDependencies.cmake index 3f4d8ca18..5dae81a18 100644 --- a/Development/cmake/NmosCppDependencies.cmake +++ b/Development/cmake/NmosCppDependencies.cmake @@ -461,7 +461,7 @@ endif() if(NMOS_CPP_USE_CONAN) set(JWT_VERSION_MIN "0.5.1") - set(JWT_VERSION_CUR "0.5.1") + set(JWT_VERSION_CUR "0.6.0") find_package(jwt-cpp REQUIRED) if(NOT jwt-cpp_VERSION) message(STATUS "Found jwt-cpp unknown version; minimum version: " ${JWT_VERSION_MIN}) From a244e0c90d3389b37a285612376a8128a5dfa685 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 30 Nov 2023 20:03:06 +0000 Subject: [PATCH 092/121] Link jwt-cpp library to nmos-cpp public --- Development/cmake/NmosCppLibraries.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 197183869..0e5a7ef82 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -1112,12 +1112,12 @@ target_link_libraries( nmos-cpp::OpenSSL nmos-cpp::cpprestsdk nmos-cpp::Boost + nmos-cpp::jwt-cpp ) target_link_libraries( nmos-cpp PRIVATE nmos-cpp::websocketpp nmos-cpp::json_schema_validator - nmos-cpp::jwt-cpp ) if(NMOS_CPP_BUILD_LLDP) target_link_libraries( From 925043cb4da813e767a27ce1438da050b9446f11 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 30 Nov 2023 20:04:57 +0000 Subject: [PATCH 093/121] Link jwt-cpp library to nmos-cpp-test --- Development/cmake/NmosCppTest.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index c6154f748..22d6e6214 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -123,6 +123,7 @@ target_link_libraries( nmos-cpp::mdns nmos-cpp::cpprestsdk nmos-cpp::Boost + nmos-cpp::jwt-cpp ) if(NMOS_CPP_BUILD_LLDP) target_link_libraries( From 01595f280a61bc9f4f77bd2029bb8d4d9ada38c0 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Sat, 2 Dec 2023 01:34:28 +0000 Subject: [PATCH 094/121] Update comment --- Development/nmos/authorization_operation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index fc101b342..7f4085608 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -190,7 +190,7 @@ namespace nmos struct authorization_exception {}; - // parse the given json to obtain access token + // parse the given json to obtain bearer token // this function is based on the oauth2_config::_parse_token_from_json(const json::value& token_json) from cpprestsdk's oauth2.cpp web::http::oauth2::experimental::oauth2_token parse_token_from_json(const web::json::value& token_json) { From 6272d59ba4f93f75d97ea4d9fd455a18ebf8497d Mon Sep 17 00:00:00 2001 From: lo-simon Date: Sat, 2 Dec 2023 01:37:40 +0000 Subject: [PATCH 095/121] Add missing jwk folder to install --- Development/cmake/NmosCppLibraries.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 0e5a7ef82..f2fb6de76 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -1148,6 +1148,7 @@ target_include_directories(nmos-cpp PUBLIC install(FILES ${NMOS_CPP_BST_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/bst) install(FILES ${NMOS_CPP_CPPREST_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/cpprest) install(FILES ${NMOS_CPP_CPPREST_DETAILS_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/cpprest/details) +install(FILES ${NMOS_CPP_JWK_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/jwk) install(FILES ${NMOS_CPP_NMOS_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/nmos) install(FILES ${NMOS_CPP_PPLX_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/pplx) install(FILES ${NMOS_CPP_RQL_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/rql) From 311bed6301b80b624407b1692a78bf92daf29f1e Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 7 Dec 2023 13:35:13 +0000 Subject: [PATCH 096/121] Ehance the usage of the get_authorization_bearer_token_handler callback --- Development/nmos-cpp-node/main.cpp | 3 +- Development/nmos/authorization_handlers.cpp | 49 +-------------- Development/nmos/authorization_handlers.h | 12 +--- Development/nmos/authorization_operation.cpp | 8 +-- Development/nmos/authorization_operation.h | 5 +- Development/nmos/client_utils.cpp | 35 ++++++----- Development/nmos/client_utils.h | 8 ++- .../nmos/connection_events_activation.cpp | 2 +- .../nmos/connection_events_activation.h | 2 +- Development/nmos/node_api_target_handler.cpp | 8 +-- Development/nmos/node_api_target_handler.h | 2 +- Development/nmos/node_behaviour.cpp | 63 +++++++++---------- Development/nmos/node_behaviour.h | 6 +- Development/nmos/node_server.cpp | 5 +- Development/nmos/node_server.h | 9 +-- 15 files changed, 78 insertions(+), 139 deletions(-) diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 81ae55209..2391f9f21 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -127,8 +127,7 @@ int main(int argc, char* argv[]) if (nmos::experimental::fields::client_authorization(node_model.settings)) { node_implementation - .on_get_authorization_bearer_token(nmos::experimental::make_authorization_token_handler(authorization_state, gate)) - .on_make_authorization_config(nmos::experimental::make_authorization_config_handler(authorization_state, gate)) + .on_get_authorization_bearer_token(nmos::experimental::make_get_authorization_bearer_token_handler(authorization_state, gate)) .on_load_authorization_clients(nmos::experimental::make_load_authorization_clients_handler(node_model.settings, gate)) .on_save_authorization_client(nmos::experimental::make_save_authorization_client_handler(node_model.settings, gate)) .on_load_rsa_private_keys(nmos::make_load_rsa_private_keys_handler(node_model.settings, gate)) // may be omitted, only required for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 085bdd9da..0674dfefc 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -207,53 +207,6 @@ namespace nmos }; } - // construct callback to make OAuth 2.0 config - authorization_config_handler make_authorization_config_handler(const web::json::value& authorization_server_metadata, const web::json::value& client_metadata, slog::base_gate& gate) - { - return[&](const web::http::oauth2::experimental::oauth2_token& bearer_token) - { - slog::log(gate, SLOG_FLF) << "Make OAuth 2.0 config"; - - web::http::oauth2::experimental::oauth2_config config( - client_metadata.is_null() ? U("") : nmos::experimental::fields::client_id(client_metadata), - client_metadata.is_null() ? U("") : client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""), - authorization_server_metadata.is_null() ? U("") : nmos::experimental::fields::authorization_endpoint(authorization_server_metadata), - authorization_server_metadata.is_null() ? U("") : nmos::experimental::fields::token_endpoint(authorization_server_metadata), - client_metadata.is_null() ? U("") : client_metadata.has_array_field(nmos::experimental::fields::redirect_uris) && nmos::experimental::fields::redirect_uris(client_metadata).size() ? nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string() : U(""), - client_metadata.is_null() ? U("") : client_metadata.has_string_field(nmos::experimental::fields::scope) ? nmos::experimental::fields::scope(client_metadata) : U("")); - - if (!client_metadata.is_null()) - { - const auto& response_types = nmos::experimental::fields::response_types(client_metadata); - bool found_code = false; - bool found_token = false; - for (auto response_type : response_types) - { - if (web::http::oauth2::experimental::response_types::code.name == response_type.as_string()) { found_code = true; } - else if (web::http::oauth2::experimental::response_types::token.name == response_type.as_string()) { found_token = true; } - }; - config.set_bearer_auth(found_code || !found_token); - } - - config.set_token(bearer_token); - - return config; - }; - } - authorization_config_handler make_authorization_config_handler(const authorization_state& authorization_state, slog::base_gate& gate) - { - return[&](const web::http::oauth2::experimental::oauth2_token& /*bearer_token*/) - { - slog::log(gate, SLOG_FLF) << "Make OAuth 2.0 config using bearer_token cache"; - - const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); - const auto client_metadata = get_client_metadata(authorization_state); - - auto make_authorization_config = make_authorization_config_handler(authorization_server_metadata, client_metadata, gate); - return make_authorization_config(authorization_state.bearer_token); - }; - } - // construct callback to validate OAuth 2.0 authorization access token validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) { @@ -364,7 +317,7 @@ namespace nmos } // construct callback to retrieve OAuth 2.0 authorization bearer token - authorization_token_handler make_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) + get_authorization_bearer_token_handler make_get_authorization_bearer_token_handler(authorization_state& authorization_state, slog::base_gate& gate) { return[&]() { diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h index 20c215807..31151e79f 100644 --- a/Development/nmos/authorization_handlers.h +++ b/Development/nmos/authorization_handlers.h @@ -135,14 +135,6 @@ namespace nmos // construct callback to start the authorization code flow request on a browser request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate); - // callback to return OAuth 2.0 authorization config - // this callback is executed while constructing http_client_config - // this callback should not throw exceptions - typedef std::function authorization_config_handler; - // construct callback to make OAuth 2.0 config - authorization_config_handler make_authorization_config_handler(const web::json::value& authorization_server_metadata, const web::json::value& client_metadata, slog::base_gate& gate); - authorization_config_handler make_authorization_config_handler(const authorization_state& authorization_state, slog::base_gate& gate); - // callback to validate OAuth 2.0 authorization access token // this callback should not throw exceptions typedef std::function validate_authorization_token_handler; @@ -158,9 +150,9 @@ namespace nmos // callback to return OAuth 2.0 authorization bearer token // this callback is execute while create http_client // this callback should not throw exceptions - typedef std::function authorization_token_handler; + typedef std::function get_authorization_bearer_token_handler; // construct callback to retrieve OAuth 2.0 authorization bearer token - authorization_token_handler make_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate); + get_authorization_bearer_token_handler make_get_authorization_bearer_token_handler(authorization_state& authorization_state, slog::base_gate& gate); } } diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 7f4085608..4b9cf3238 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -180,9 +180,9 @@ namespace nmos // construct authorization client config based on settings // with the remaining options defaulted, e.g. authorization request timeout - web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) + web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings, load_ca_certificates, make_authorization_config, bearer_token, gate); + auto config = nmos::make_http_client_config(settings, load_ca_certificates, bearer_token, gate); config.set_timeout(std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings))); return config; @@ -1323,7 +1323,7 @@ namespace nmos const auto& registration_client_uri = nmos::experimental::fields::registration_client_uri(client_metadata); const auto& issuer = nmos::experimental::fields::issuer(authorization_server_metadata); - request = request_client_metadata_from_openid_connect(web::http::client::http_client(registration_client_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, make_authorization_config_handler(authorization_server_metadata, client_metadata, gate), { registration_access_token }, gate)), + request = request_client_metadata_from_openid_connect(web::http::client::http_client(registration_client_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, { registration_access_token }, gate)), auth_version, gate, token).then([&model, &authorization_state, issuer, save_authorization_client, &gate](web::json::value client_metadata) { auto lock = model.write_lock(); @@ -1474,7 +1474,7 @@ namespace nmos const auto jwks_uri = make_jwks_uri(model.settings); const auto& initial_access_token = nmos::experimental::fields::initial_access_token(model.settings); - request = request_client_registration(web::http::client::http_client(registration_endpoint, make_authorization_http_client_config(model.settings, load_ca_certificates, make_authorization_config_handler({}, {}, gate), { initial_access_token }, gate)), + request = request_client_registration(web::http::client::http_client(registration_endpoint, make_authorization_http_client_config(model.settings, load_ca_certificates, { initial_access_token }, gate)), client_name, redirect_uris, {}, response_types, scopes, grants, token_endpoint_auth_method, {}, jwks_uri, auth_version, gate, token).then([&model, &authorization_state, issuer, token_endpoint_auth_method, save_authorization_client, &gate](web::json::value client_metadata) { auto lock = model.write_lock(); diff --git a/Development/nmos/authorization_operation.h b/Development/nmos/authorization_operation.h index eadc01c9d..b522d5df9 100644 --- a/Development/nmos/authorization_operation.h +++ b/Development/nmos/authorization_operation.h @@ -32,12 +32,13 @@ namespace nmos namespace details { + // construct authorization client config based on settings // with the remaining options defaulted, e.g. authorization request timeout - web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); + web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); inline web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - return make_authorization_http_client_config(settings, load_ca_certificates, {}, {}, gate); + return make_authorization_http_client_config(settings, load_ca_certificates, {}, gate); } // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index 707df9104..74df9b1bb 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -179,36 +179,41 @@ namespace nmos return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate); } - // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy + // construct oauth2 config with the bearer token + web::http::oauth2::experimental::oauth2_config make_oauth2_config(const web::http::oauth2::experimental::oauth2_token& bearer_token) + { + web::http::oauth2::experimental::oauth2_config config(U(""), U(""), U(""), U(""), U(""), U("")); + config.set_token(bearer_token); + + return config; + } + + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. authorization request timeout - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) { auto config = make_http_client_config(settings, load_ca_certificates, gate); - if (make_authorization_config) + if (bearer_token.is_valid_access_token()) { - auto oauth2_config = make_authorization_config({}); - if (oauth2_config.token().is_valid_access_token()) - { - config.set_oauth2(make_authorization_config({})); - } + config.set_oauth2(make_oauth2_config(bearer_token)); } return config; } - // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. authorization request timeout - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) { - auto config = make_http_client_config(settings, load_ca_certificates, gate); + web::http::oauth2::experimental::oauth2_token bearer_token; - if (make_authorization_config && bearer_token.is_valid_access_token()) + if (get_authorization_bearer_token) { - config.set_oauth2(make_authorization_config(bearer_token)); + bearer_token = get_authorization_bearer_token(); } - return config; + return make_http_client_config(settings, load_ca_certificates, bearer_token, gate); } // construct client config based on specified secure flag and settings, e.g. using the specified proxy @@ -238,7 +243,7 @@ namespace nmos // construct client config based on settings and access token, e.g. using the specified proxy // with the remaining options defaulted - web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate) + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) { auto config = make_websocket_client_config(settings, std::move(load_ca_certificates), gate); diff --git a/Development/nmos/client_utils.h b/Development/nmos/client_utils.h index 5c21c16b5..284846c8a 100644 --- a/Development/nmos/client_utils.h +++ b/Development/nmos/client_utils.h @@ -17,16 +17,18 @@ namespace nmos web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate); web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); // construct client config based on specified secure flag and settings, e.g. using the specified proxy // with the remaining options defaulted web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted - web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // make an API request with logging diff --git a/Development/nmos/connection_events_activation.cpp b/Development/nmos/connection_events_activation.cpp index cafc6e2bf..8bea107e8 100644 --- a/Development/nmos/connection_events_activation.cpp +++ b/Development/nmos/connection_events_activation.cpp @@ -11,7 +11,7 @@ namespace nmos { // this handler can be used to (un)subscribe IS-07 Events WebSocket receivers with the specified handlers, when they are activated - nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::authorization_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate) + nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate) { std::shared_ptr events_ws_client(new nmos::events_ws_client(nmos::make_websocket_client_config(settings, load_ca_certificates, get_authorization_bearer_token, gate), nmos::fields::events_heartbeat_interval(settings), gate)); diff --git a/Development/nmos/connection_events_activation.h b/Development/nmos/connection_events_activation.h index 573df6e14..4367ab2de 100644 --- a/Development/nmos/connection_events_activation.h +++ b/Development/nmos/connection_events_activation.h @@ -12,7 +12,7 @@ namespace nmos struct node_model; // this handler can be used to (un)subscribe IS-07 Events WebSocket receivers with the specified handlers, when they are activated - nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::authorization_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate); + nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate); inline nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::load_ca_certificates_handler load_ca_certificates, nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) { diff --git a/Development/nmos/node_api_target_handler.cpp b/Development/nmos/node_api_target_handler.cpp index 30cd64e88..a98d26967 100644 --- a/Development/nmos/node_api_target_handler.cpp +++ b/Development/nmos/node_api_target_handler.cpp @@ -13,11 +13,11 @@ namespace nmos { - // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser and the specified validator + // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser, the specified validator and the bearer token getter // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) - node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::authorization_config_handler make_authorization_config) + node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token) { - return [&model, load_ca_certificates, parse_transport_file, validate_merged, make_authorization_config](const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate) + return [&model, load_ca_certificates, parse_transport_file, validate_merged, get_authorization_bearer_token](const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate) { using web::json::value; using web::json::value_of; @@ -30,7 +30,7 @@ namespace nmos // if manifest_href is null, this will throw json_exception which will be reported appropriately as 400 Bad Request const auto manifest_href = nmos::fields::manifest_href(sender_data).as_string(); - web::http::client::http_client client(manifest_href, nmos::with_read_lock(model.mutex, [&, load_ca_certificates, make_authorization_config] { return nmos::make_http_client_config(model.settings, load_ca_certificates, make_authorization_config, gate); })); + web::http::client::http_client client(manifest_href, nmos::with_read_lock(model.mutex, [&, load_ca_certificates, get_authorization_bearer_token] { return nmos::make_http_client_config(model.settings, load_ca_certificates, get_authorization_bearer_token, gate); })); return api_request(client, web::http::methods::GET, gate).then([manifest_href, &gate](web::http::http_response res) { if (res.status_code() != web::http::status_codes::OK) diff --git a/Development/nmos/node_api_target_handler.h b/Development/nmos/node_api_target_handler.h index ac368ec06..2b22990d7 100644 --- a/Development/nmos/node_api_target_handler.h +++ b/Development/nmos/node_api_target_handler.h @@ -22,7 +22,7 @@ namespace nmos // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified handlers // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) - node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::authorization_config_handler make_authorization_config); + node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token); inline node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) { diff --git a/Development/nmos/node_behaviour.cpp b/Development/nmos/node_behaviour.cpp index 355860a6f..ba86524e4 100644 --- a/Development/nmos/node_behaviour.cpp +++ b/Development/nmos/node_behaviour.cpp @@ -22,11 +22,11 @@ namespace nmos { namespace details { - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); // registered operation - void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate); - void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); + void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); + void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); // peer to peer operation void peer_to_peer_operation(nmos::model& model, const nmos::id& grain_id, mdns::service_discovery& discovery, mdns::service_advertiser& advertiser, slog::base_gate& gate); @@ -43,7 +43,7 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate_) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); @@ -52,7 +52,7 @@ namespace nmos mdns::service_discovery discovery(gate); - details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(make_authorization_config), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(get_authorization_bearer_token), advertiser, discovery, gate); } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate_) { @@ -63,43 +63,35 @@ namespace nmos mdns::service_discovery discovery(gate); - details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, {}, advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, advertiser, discovery, gate); } // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate_) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); - details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(make_authorization_config), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(get_authorization_bearer_token), advertiser, discovery, gate); } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { - node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, {}, advertiser, discovery, gate); + node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, advertiser, discovery, gate); } // uses the default DNS-SD implementation - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate) - { - node_behaviour_thread(model, load_ca_certificates, {}, std::move(make_authorization_config), std::move(get_authorization_bearer_token), gate); - } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - node_behaviour_thread(model, load_ca_certificates, {}, gate); + node_behaviour_thread(model, load_ca_certificates, {}, {}, gate); } // uses the specified DNS-SD implementation - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) - { - node_behaviour_thread(model, load_ca_certificates, {}, std::move(make_authorization_config), std::move(get_authorization_bearer_token), advertiser, discovery, gate); - } void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { - node_behaviour_thread(model, load_ca_certificates, {}, advertiser, discovery, gate); + node_behaviour_thread(model, load_ca_certificates, {}, {}, advertiser, discovery, gate); } - void details::node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + void details::node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { // The possible states of node behaviour represent the two primary modes (registered operation and peer-to-peer operation) // and a few hopefully ephemeral states as the node works through the "Standard Registration Sequences". @@ -184,7 +176,7 @@ namespace nmos case initial_registration: // "5. The Node registers itself with the Registration API by taking the object it holds under the Node API's /self resource and POSTing this to the Registration API." - details::initial_registration(self_id, model, grain_id, load_ca_certificates, make_authorization_config, gate); + details::initial_registration(self_id, model, grain_id, load_ca_certificates, get_authorization_bearer_token, gate); if (details::has_discovered_registration_services(model)) { @@ -201,7 +193,7 @@ namespace nmos case registered_operation: // "6. The Node persists itself in the registry by issuing heartbeats." // "7. The Node registers its other resources (from /devices, /sources etc) with the Registration API." - details::registered_operation(self_id, model, grain_id, load_ca_certificates, registration_changed, make_authorization_config, get_authorization_bearer_token, gate); + details::registered_operation(self_id, model, grain_id, load_ca_certificates, registration_changed, get_authorization_bearer_token, gate); if (details::has_discovered_registration_services(model)) { @@ -494,16 +486,16 @@ namespace nmos handle_registration_error_conditions(response, false, gate, operation); } - web::http::client::http_client_config make_registration_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) + web::http::client::http_client_config make_registration_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), std::move(make_authorization_config), gate); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), bearer_token, gate); config.set_timeout(std::chrono::seconds(nmos::fields::registration_request_max(settings))); return config; } - web::http::client::http_client_config make_heartbeat_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) + web::http::client::http_client_config make_heartbeat_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), std::move(make_authorization_config), gate); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), bearer_token, gate); config.set_timeout(std::chrono::seconds(nmos::fields::registration_heartbeat_max(settings))); return config; } @@ -681,7 +673,7 @@ namespace nmos } // there is significant similarity between initial_registration and registered_operation but I'm too tired to refactor again right now... - void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, slog::base_gate& gate) + void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting initial registration"; @@ -768,7 +760,7 @@ namespace nmos grain.updated = strictly_increasing_update(resources); }); - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, get_authorization_bearer_token(), gate))); } events = web::json::value::array(); @@ -839,7 +831,7 @@ namespace nmos request.wait(); } - void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate) + void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Adopting registered operation"; @@ -908,8 +900,9 @@ namespace nmos const auto registry_version = parse_api_version(web::uri::split_path(base_uri.path()).back()); if (registry_version != grain->version) break; - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); - heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + const auto& bearer_token = get_authorization_bearer_token(); + registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, bearer_token, gate))); + heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, bearer_token, gate))); // "The first interaction with a new Registration API [after a server side or connectivity issue] // should be a heartbeat to confirm whether whether the Node is still present in the registry" @@ -955,13 +948,13 @@ namespace nmos if (get_authorization_bearer_token) { - const auto bearer_token = get_authorization_bearer_token(); + const auto& bearer_token = get_authorization_bearer_token(); if (heartbeat_bearer_token.access_token() != bearer_token.access_token()) { slog::log(gate, SLOG_FLF) << "Update heartbeat client with new authorization token"; heartbeat_bearer_token = bearer_token; - heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, bearer_token, gate))); } } @@ -1013,13 +1006,13 @@ namespace nmos // renew regsitration_client if bearer token has changed if (get_authorization_bearer_token) { - const auto bearer_token = get_authorization_bearer_token(); + const auto& bearer_token = get_authorization_bearer_token(); if (registration_bearer_token.access_token() != bearer_token.access_token()) { slog::log(gate, SLOG_FLF) << "Update registration client with new authorization token"; registration_bearer_token = bearer_token; - registration_client.reset(new web::http::client::http_client(registration_client->base_uri(), make_registration_client_config(model.settings, load_ca_certificates, make_authorization_config, gate))); + registration_client.reset(new web::http::client::http_client(registration_client->base_uri(), make_registration_client_config(model.settings, load_ca_certificates, bearer_token, gate))); } } diff --git a/Development/nmos/node_behaviour.h b/Development/nmos/node_behaviour.h index 53293eb2d..c98abf854 100644 --- a/Development/nmos/node_behaviour.h +++ b/Development/nmos/node_behaviour.h @@ -36,20 +36,18 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate); // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); // uses the default DNS-SD implementation - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // uses the specified DNS-SD implementation - void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, nmos::experimental::authorization_config_handler make_authorization_config, nmos::experimental::authorization_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); } diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 0d0460efe..6d8d5fb99 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -49,7 +49,7 @@ namespace nmos // Configure the Node API - nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged, node_implementation.make_authorization_config); + nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged, node_implementation.get_authorization_bearer_token); auto validate_authorization = node_implementation.validate_authorization; node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, validate_authorization ? validate_authorization(nmos::experimental::scopes::node) : nullptr, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); @@ -106,10 +106,9 @@ namespace nmos auto set_transportfile = node_implementation.set_transportfile; auto connection_activated = node_implementation.connection_activated; auto channelmapping_activated = node_implementation.channelmapping_activated; - auto make_authorization_config = node_implementation.make_authorization_config; auto get_authorization_bearer_token = node_implementation.get_authorization_bearer_token; node_server.thread_functions.assign({ - [&, load_ca_certificates, registration_changed, make_authorization_config, get_authorization_bearer_token] { nmos::node_behaviour_thread(node_model, load_ca_certificates, registration_changed, make_authorization_config, get_authorization_bearer_token, gate); }, + [&, load_ca_certificates, registration_changed, get_authorization_bearer_token] { nmos::node_behaviour_thread(node_model, load_ca_certificates, registration_changed, get_authorization_bearer_token, gate); }, [&] { nmos::send_events_ws_messages_thread(events_ws_listener, node_model, events_ws_api.second, gate); }, [&] { nmos::erase_expired_events_resources_thread(node_model, gate); }, [&, resolve_auto, set_transportfile, connection_activated] { nmos::connection_activation_thread(node_model, resolve_auto, set_transportfile, connection_activated, gate); }, diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index ea8474a8f..6f7d3a176 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -26,7 +26,7 @@ namespace nmos // underlying implementation into the server instance for the NMOS Node struct node_implementation { - node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, authorization_config_handler make_authorization_config, authorization_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code) + node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, get_authorization_bearer_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code) : load_server_certificates(std::move(load_server_certificates)) , load_dh_param(std::move(load_dh_param)) , load_ca_certificates(std::move(load_ca_certificates)) @@ -38,7 +38,6 @@ namespace nmos , set_transportfile(std::move(set_transportfile)) , connection_activated(std::move(connection_activated)) , get_ocsp_response(std::move(get_ocsp_response)) - , make_authorization_config(std::move(make_authorization_config)) , get_authorization_bearer_token(std::move(get_authorization_bearer_token)) , validate_authorization(std::move(validate_authorization)) , ws_validate_authorization(std::move(ws_validate_authorization)) @@ -67,8 +66,7 @@ namespace nmos node_implementation& on_validate_channelmapping_output_map(nmos::details::channelmapping_output_map_validator validate_map) { this->validate_map = std::move(validate_map); return *this; } node_implementation& on_channelmapping_activated(nmos::channelmapping_activation_handler channelmapping_activated) { this->channelmapping_activated = std::move(channelmapping_activated); return *this; } node_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } - node_implementation& on_make_authorization_config(authorization_config_handler make_authorization_config) { this->make_authorization_config = std::move(make_authorization_config); return *this; } - node_implementation& on_get_authorization_bearer_token(authorization_token_handler get_authorization_bearer_token) { this->get_authorization_bearer_token = std::move(get_authorization_bearer_token); return *this; } + node_implementation& on_get_authorization_bearer_token(get_authorization_bearer_token_handler get_authorization_bearer_token) { this->get_authorization_bearer_token = std::move(get_authorization_bearer_token); return *this; } node_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return *this; } node_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } node_implementation& on_load_rsa_private_keys(nmos::load_rsa_private_keys_handler load_rsa_private_keys) { this->load_rsa_private_keys = std::move(load_rsa_private_keys); return *this; } @@ -105,8 +103,7 @@ namespace nmos nmos::ocsp_response_handler get_ocsp_response; - authorization_config_handler make_authorization_config; - authorization_token_handler get_authorization_bearer_token; + get_authorization_bearer_token_handler get_authorization_bearer_token; validate_authorization_handler validate_authorization; ws_validate_authorization_handler ws_validate_authorization; nmos::load_rsa_private_keys_handler load_rsa_private_keys; From 5ee5b307c79bc5da5d03ab784fec85d92aa8f570 Mon Sep 17 00:00:00 2001 From: Simon Lo Date: Fri, 8 Dec 2023 01:14:01 +0000 Subject: [PATCH 097/121] Apply suggestions from code review Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- Development/cpprest/access_token_error.h | 2 +- Development/nmos/api_utils.cpp | 8 +++---- Development/nmos/authorization.cpp | 2 +- Development/nmos/authorization_behaviour.cpp | 22 ++++++++++---------- Development/nmos/authorization_handlers.cpp | 6 +++--- Development/nmos/authorization_handlers.h | 2 +- Development/nmos/authorization_operation.cpp | 18 ++++++++-------- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Development/cpprest/access_token_error.h b/Development/cpprest/access_token_error.h index 6ed7ceed0..0fa94f2a3 100644 --- a/Development/cpprest/access_token_error.h +++ b/Development/cpprest/access_token_error.h @@ -20,7 +20,7 @@ namespace web // "application/x-www-form-urlencoded" format" // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 - // for diret error + // for direct error: // If the access token request is invalid or unauthorized // "The authorization server responds with an HTTP 400 (Bad Request) // status code(unless specified otherwise) and includes the following diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index 50351beb8..36602b626 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -822,12 +822,12 @@ namespace nmos utility::string_t error_description{}; // If the request lacks any authentication information (e.g., the client // was unaware that authentication is necessary or attempted using an - // unsupported authentication method), the resource server SHOULD NOT - // include an error code or other error information. + // unsupported authentication method), the resource server SHOULD NOT + // include an error code or other error information. // - // For example : + // For example : // - // HTTP / 1.1 401 Unauthorized + // HTTP / 1.1 401 Unauthorized // WWW - Authenticate : Bearer realm = "example" // see https://tools.ietf.org/html/rfc6750#section-3.1 if (error.value != nmos::experimental::authorization_error::without_authentication) diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp index 46bd68c99..0a3510d03 100644 --- a/Development/nmos/authorization.cpp +++ b/Development/nmos/authorization.cpp @@ -43,7 +43,7 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Test token expiry error: " << e.what(); } - // reaching here, token validation has failed, treat it as expired + // reaching here indicates token validation has failed so treat it as expired return true; } diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp index 1d59c4b53..947ddb38a 100644 --- a/Development/nmos/authorization_behaviour.cpp +++ b/Development/nmos/authorization_behaviour.cpp @@ -80,7 +80,7 @@ namespace nmos std::default_random_engine discovery_backoff_engine(discovery_backoff_seeder); double discovery_backoff = 0; - // load authorization clients metadata to cache + // load authorization client's metadata to cache if (load_authorization_clients) { const auto auth_clients = load_authorization_clients(); @@ -138,10 +138,10 @@ namespace nmos // reterive client metadata from cache const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); - // is it not a scopeless client (where scopeless client doesn't access any protected APIs, i.e. doesn't require to register to Authorization server) + // does the client have a scope? A client without a scope is one that doesn't access any protected APIs (i.e. client isn't required to register with Authorization server). if (with_read_lock(model.mutex, [&] { return details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)).size(); })) { - // is the client already registered to Authorization server, i.e. found it in cache + // is the client already registered to Authorization server? (i.e. found it in cache). if (!client_metadata.is_null()) { // no token or token expired @@ -180,8 +180,8 @@ namespace nmos { // if OpenID Connect Authorization server is used, client status can be obtained via the Client Configuration Endpoint // "The Client Configuration Endpoint is an OAuth 2.0 Protected Resource that MAY be provisioned by the server for a - // specific Client to be able to view and update its registered information." - // see 3.2 of https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint + // specific Client to be able to view and update its registered information." + // see https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint // registration_access_token // OPTIONAL. Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the // Client registration. @@ -208,7 +208,7 @@ namespace nmos } else { - // no registration_access_token and registration_client_uri found, treat it has connected with a non-OpenID Connect server + // no registration_access_token and registration_client_uri found, treat it as if connected with a non-OpenID Connect server // start grant flow based on what been defined in the settings // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration mode = is_client_expired() ? client_registration // client registration @@ -227,13 +227,13 @@ namespace nmos } else { - // client has not been registered to the Authorization server yet + // client has not been registered with the Authorization server yet mode = client_registration; } } else { - // scope-less client, not require to obtain access token + // client does not have a scope therefore not require to obtain access token mode = authorization_operation; } } @@ -278,10 +278,10 @@ namespace nmos case authorization_operation: // fetch public keys - // fetch access token in 1/2 token life time interval + // fetch access token within 1/2 token life time interval. details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, false, gate); - // reaching here, there must be failure within the authorization operation, + // reaching here indicates there has been a failure within the authorization operation, // start the authorization sequence again on next available Authorization server authorization_service_error = true; mode = request_authorization_server_metadata; @@ -292,7 +292,7 @@ namespace nmos // immediately fetch access token details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, true, gate); - // reaching here, there must be failure within the authorization operation, + // reaching here indicates there has been a failure within the authorization operation, // start the authorization sequence again on next available Authorization server authorization_service_error = true; mode = request_authorization_server_metadata; diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp index 0674dfefc..6b155b4b8 100644 --- a/Development/nmos/authorization_handlers.cpp +++ b/Development/nmos/authorization_handlers.cpp @@ -182,10 +182,10 @@ namespace nmos } // construct callback to start the authorization code flow request on a browser - // it is required for those OAuth client which is using the Authorization Code Flow to obtain the access token + // this is required for OAuth clients which use Authorization Code Flow to obtain the access token // note: as it is not easy to specify the 'content-type' used in the browser programmatically, this can be easily // fixed by installing a browser header modifier - // such extension e.g. ModHeader can be used to add the missing 'content-type' header accordingly + // extensions such as ModHeader can be used to add the missing 'content-type' header: // for Windows https://chrome.google.com/webstore/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj // for Linux https://addons.mozilla.org/en-GB/firefox/addon/modheader-firefox/ request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate) @@ -227,7 +227,7 @@ namespace nmos try { - // if jwt_validator has not already set up, treat it as no public keys to validate token + // if jwt_validator is not already set up, assume no public keys to validate token if (issuer->second.jwt_validator.is_initialized()) { // do access token basic validation, including token schema validation and token issuer public keys validation diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h index 31151e79f..b21a27dd7 100644 --- a/Development/nmos/authorization_handlers.h +++ b/Development/nmos/authorization_handlers.h @@ -36,7 +36,7 @@ namespace nmos { succeeded, without_authentication, // failure: access protected resource request without authentication - insufficient_scope, // failure: access protected resource request higher privileges + insufficient_scope, // failure: access protected resource request requires higher privileges no_matching_keys, // failure: no matching keys for the token validation failed // failure: access protected resource request with authentication but failed }; diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index 4b9cf3238..c7f14ffcf 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -100,8 +100,8 @@ namespace nmos // code_verifier = high-entropy cryptographic random STRING using the // unreserved characters[A - Z] / [a - z] / [0 - 9] / "-" / "." / "_" / "~" - // from Section 2.3 of[RFC3986], with a minimum length of 43 characters - // and a maximum length of 128 characters + // from Section 2.3 of[RFC3986], with a minimum length of 43 characters + // and a maximum length of 128 characters // see https://tools.ietf.org/html/rfc7636#section-4.1 { utility::nonce_generator generator(128); @@ -136,7 +136,7 @@ namespace nmos return ub.to_uri(); } - // it is used to strip the trailing dot of the FQDN if it is presented + // used to strip the trailing dot of the FQDN if it is presented utility::string_t strip_trailing_dot(const utility::string_t& host_) { auto host = host_; @@ -219,7 +219,7 @@ namespace nmos } else { - // Some services don't return 'token_type' while it's required by OAuth 2.0 spec: + // Some services don't return 'token_type' even though it's required by the OAuth 2.0 spec: // http://tools.ietf.org/html/rfc6749#section-5.1 // As workaround we act as if 'token_type=bearer' was received. result.set_token_type(oauth2_strings::bearer); @@ -301,9 +301,9 @@ namespace nmos // validate server metadata authapi_validator().validate(metadata, experimental::make_authapi_auth_metadata_schema_uri(version)); // may throw json_exception - // hmm, verify Authorization server meeting the minimum client requirement + // hmm, verify Authorization server meets the minimum client requirement. - // is the required response_types supported by the Authorization server + // are the required response_types supported by the Authorization server? std::set response_types = { response_types::code }; if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return grant_types::implicit == grant; })) { @@ -327,7 +327,7 @@ namespace nmos // scopes_supported is optional if (scopes.size() && metadata.has_array_field(nmos::experimental::fields::scopes_supported)) { - // is the required scopes supported by the Authorization server + // are the required scopes supported by the Authorization server? const auto supported = std::all_of(scopes.begin(), scopes.end(), [&](const nmos::experimental::scope& scope) { const auto& scopes_supported = nmos::experimental::fields::scopes_supported(metadata); @@ -336,7 +336,7 @@ namespace nmos }); if (!supported) { - slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting all the required scopes: " << [&scopes]() { std::stringstream ss; for (auto scope : scopes) ss << utility::us2s(scope.name) << " "; return ss.str(); }(); + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not support all the required scopes: " << [&scopes]() { std::stringstream ss; for (auto scope : scopes) ss << utility::us2s(scope.name) << " "; return ss.str(); }(); throw authorization_exception(); } } @@ -353,7 +353,7 @@ namespace nmos }); if (!supported) { - slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting all the required grants: " << [&grants]() { std::stringstream ss; for (auto grant : grants) ss << utility::us2s(grant.name) << " "; return ss.str(); }(); + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not support all the required grants: " << [&grants]() { std::stringstream ss; for (auto grant : grants) ss << utility::us2s(grant.name) << " "; return ss.str(); }(); throw authorization_exception(); } } From 86df138a706f58165c03ded7f92c1bbd75213cb4 Mon Sep 17 00:00:00 2001 From: Simon Lo Date: Wed, 13 Dec 2023 11:57:19 +0000 Subject: [PATCH 098/121] Apply suggestions from code review Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- Development/nmos/authorization_behaviour.cpp | 5 ++ Development/nmos/authorization_operation.cpp | 72 +++++++++---------- Development/nmos/authorization_operation.h | 4 +- .../nmos/authorization_redirect_api.cpp | 7 +- Development/nmos/authorization_state.h | 4 +- Development/nmos/authorization_utils.cpp | 2 +- Development/nmos/certificate_handlers.cpp | 2 +- Development/nmos/client_utils.cpp | 2 +- Development/nmos/events_ws_api.cpp | 2 +- Development/nmos/jwk_utils.cpp | 2 +- Development/nmos/jwt_validator_impl.cpp | 10 +-- 11 files changed, 58 insertions(+), 54 deletions(-) diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp index 947ddb38a..f5bdeead3 100644 --- a/Development/nmos/authorization_behaviour.cpp +++ b/Development/nmos/authorization_behaviour.cpp @@ -279,6 +279,8 @@ namespace nmos case authorization_operation: // fetch public keys // fetch access token within 1/2 token life time interval. + // authorization_operation will block until an error occurs, or shutdown + // on shutdown, enclosing for loop will exit details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, false, gate); // reaching here indicates there has been a failure within the authorization operation, @@ -290,6 +292,9 @@ namespace nmos case authorization_operation_with_immediate_token_fetch: // fetch public keys // immediately fetch access token + // authorization_operation will block until an error occurs, or shutdown + // on shutdown, enclosing for loop will exit + details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, true, gate); // reaching here indicates there has been a failure within the authorization operation, diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp index c7f14ffcf..aab25170c 100644 --- a/Development/nmos/authorization_operation.cpp +++ b/Development/nmos/authorization_operation.cpp @@ -78,7 +78,7 @@ namespace nmos return{}; } - // use the authorization URI on a web browser to start the authorization code grant workflow + // use the authorization URI on a web browser to start the authorization code flow web::uri make_authorization_code_uri(const web::uri& authorization_endpoint, const utility::string_t& client_id, const web::uri& redirect_uri, const web::http::oauth2::experimental::response_type& response_type, const std::set& scopes, const web::json::array& code_challenge_methods_supported, utility::string_t& state, utility::string_t& code_verifier) { using web::http::oauth2::details::oauth2_strings; @@ -344,7 +344,7 @@ namespace nmos // grant_types_supported is optional if (grants.size() && metadata.has_array_field(nmos::experimental::fields::grant_types_supported)) { - // is the required grants supported by the Authorization server + // are the required grants supported by the Authorization server? const auto supported = std::all_of(grants.begin(), grants.end(), [&](const web::http::oauth2::experimental::grant_type& grant) { const auto& grants_supported = nmos::experimental::fields::grant_types_supported(metadata); @@ -361,7 +361,7 @@ namespace nmos // token_endpoint_auth_methods_supported is optional if (metadata.has_array_field(nmos::experimental::fields::token_endpoint_auth_methods_supported)) { - // is the required token_endpoint_auth_method supported by the Authorization server + // is the required token_endpoint_auth_method supported by the Authorization server? const auto& supported = nmos::experimental::fields::token_endpoint_auth_methods_supported(metadata); const auto found = std::find_if(supported.begin(), supported.end(), [&token_endpoint_auth_method](const web::json::value& token_endpoint_auth_method_) { return token_endpoint_auth_method_.as_string() == token_endpoint_auth_method.name; }); if (supported.end() == found) @@ -1011,8 +1011,8 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request unexpected unknown exception"; } - // reaching here, there must be something has gone wrong with the Authorization Server - // let select the next avaliable Authorization server + // reaching here indicates something has gone wrong with the Authorization Server + // so let's select the next available Authorization server authorization_service_error = true; model.notify(); @@ -1155,7 +1155,7 @@ namespace nmos }); } - // fetch authorization server metadata, such as endpoints use for client registration, token fetch and public keys fetch + // fetch authorization server metadata, such as endpoints used for client registration, token fetches and public keys fetches bool request_authorization_server_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, bool& authorization_service_error, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting authorization server metadata fetch"; @@ -1338,27 +1338,27 @@ namespace nmos throw authorization_exception(); } - // scope is optional, it may not be returned by the Authorization server, just insert it, - // as it is required for the authorization support + // scope is optional. If one has not be returned by the Authorization server, + // insert one as it is required by authorization functionality. if (!client_metadata.has_field(nmos::experimental::fields::scope)) { client_metadata[nmos::experimental::fields::scope] = web::json::value::string(make_scope(nmos::experimental::authorization_scopes::from_settings(model.settings))); } - // grant_types is optional, it may not be returned by the Authorization server, just insert it, - // as it is required for the authorization support + // grant_types is optional. If it has not been returned by the Authorization server + // insert it as it is required by authorization functionality. if (!client_metadata.has_field(nmos::experimental::fields::grant_types)) { client_metadata[nmos::experimental::fields::grant_types] = make_grant_types(grant_types_from_settings(model.settings)); } - // token_endpoint_auth_method is optional, it may not be returning by the Authorization server, just insert it, - // as it is required for the authorization support + // token_endpoint_auth_method is optional. If it has not been returned by the Authorization server + // insert it as it is required by the authorization functionality. if (!client_metadata.has_field(nmos::experimental::fields::token_endpoint_auth_method)) { client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); } // store client metadata to settings - // hmm, may store the only required fields + // hmm, may store only the required fields nmos::experimental::update_client_metadata(authorization_state, client_metadata); // do callback to safely store the client metadata @@ -1427,7 +1427,7 @@ namespace nmos return !authorization_service_error && registered; } - // register client to the Authorization server + // register client with the Authorization server bool client_registration(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler save_authorization_client, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting authorization client registration"; @@ -1479,7 +1479,7 @@ namespace nmos { auto lock = model.write_lock(); - // check client_secret existence for confidential client + // check client_secret exists for confidential client if (client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method)) { if (((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name) @@ -1503,27 +1503,27 @@ namespace nmos } } - // scope is optional, it may not be returned by the Authorization server, just insert it, - // as it is required for the authorization support + // scope is optional. If one has not be returned by the Authorization server, + // insert one as it is required by authorization functionality. if (!client_metadata.has_field(nmos::experimental::fields::scope)) { client_metadata[nmos::experimental::fields::scope] = web::json::value::string(make_scope(nmos::experimental::authorization_scopes::from_settings(model.settings))); } - // grant_types is optional, it may not be returned by the Authorization server, just insert it, - // as it is required for the authorization support + // grant_types is optional. If it has not been returned by the Authorization server + // insert it as it is required by authorization functionality. if (!client_metadata.has_field(nmos::experimental::fields::grant_types)) { client_metadata[nmos::experimental::fields::grant_types] = make_grant_types(grant_types_from_settings(model.settings)); } - // token_endpoint_auth_method is optional, it may not be returning by the Authorization server, just insert it, - // as it is required for the authorization support + // token_endpoint_auth_method is optional. If it has not been returned by the Authorization server + // insert it as it is required by the authorization functionality. if (!client_metadata.has_field(nmos::experimental::fields::token_endpoint_auth_method)) { client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); } // store client metadata to settings - // hmm, may store the only required fields + // hmm, may store only the required fields nmos::experimental::update_client_metadata(authorization_state, client_metadata); // hmm, do a callback allowing user to store the client credentials @@ -1591,7 +1591,7 @@ namespace nmos return !authorization_service_error && registered; } - // start authorization code workflow + // start authorization code flow // see https://tools.ietf.org/html/rfc8252#section-4.1 bool authorization_code_flow(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::request_authorization_code_handler request_authorization_code, slog::base_gate& gate) { @@ -1616,8 +1616,8 @@ namespace nmos auto access_token_received = false; auto authorization_flow = nmos::experimental::authorization_state::request_code; - // start the authorization code grant workflow, the authorization URI is required to - // be loaded in the web browser to kick start the authorization code grant workflow + // start the authorization code flow, the authorization URI is required to + // be loaded in the web browser to kick start the authorization code grant flow if (request_authorization_code) { nmos::with_write_lock(authorization_state.mutex, [&] @@ -1630,14 +1630,14 @@ namespace nmos const auto& authorization_code_flow_max = nmos::experimental::fields::authorization_code_flow_max(settings); if (authorization_code_flow_max > -1) { - // wait access token with timeout + // wait for access token with timeout if (!model.wait_for(lock, std::chrono::seconds(authorization_code_flow_max), [&] { authorization_flow = with_read_lock(authorization_state.mutex, [&] { return authorization_state.authorization_flow; }); return shutdown || nmos::experimental::authorization_state::failed == authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_flow; })) { - // authorization code workflow timeout + // authorization code flow timeout authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "Authorization code workflow timeout"; + slog::log(gate, SLOG_FLF) << "Authorization code flow timeout"; } else if (nmos::experimental::authorization_state::access_token_received == authorization_flow) { @@ -1647,14 +1647,14 @@ namespace nmos } else { - // authorization code workflow failure + // authorization code flow failure authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "Authorization code workflow failure"; + slog::log(gate, SLOG_FLF) << "Authorization code flow failure"; } } else { - // wait access token without timeout + // wait for access token without timeout condition.wait(lock, [&] { authorization_flow = with_read_lock(authorization_state.mutex, [&] { return authorization_state.authorization_flow; }); return shutdown || nmos::experimental::authorization_state::failed == authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_flow; }); @@ -1667,17 +1667,17 @@ namespace nmos } else { - // authorization code workflow failure + // authorization code flow failure authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "Authorization code workflow failure"; + slog::log(gate, SLOG_FLF) << "Authorization code flow failure"; } } } else { - // no handler to start the authorization code grant workflow + // no handler to start the authorization code grant flow authorization_service_error = true; - slog::log(gate, SLOG_FLF) << "No authorization code workflow handler"; + slog::log(gate, SLOG_FLF) << "No authorization code flow handler"; } model.notify(); @@ -1688,7 +1688,7 @@ namespace nmos // fetch the bearer access token for the required scope(s) to access the protected APIs // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#requesting-a-token // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#accessing-protected-resources - // fetch the token issuer(authorization server)'s public keys fpr validating the incoming bearer access token + // fetch the token issuer(authorization server)'s public keys for validating the incoming bearer access token // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate) { diff --git a/Development/nmos/authorization_operation.h b/Development/nmos/authorization_operation.h index b522d5df9..9bc1dacec 100644 --- a/Development/nmos/authorization_operation.h +++ b/Development/nmos/authorization_operation.h @@ -56,7 +56,7 @@ namespace nmos // see https://tools.ietf.org/html/rfc7591#section-3.1 bool client_registration(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler client_registered, slog::base_gate& gate); - // start authorization code workflow + // start authorization code flow // see https://tools.ietf.org/html/rfc8252#section-4.1 bool authorization_code_flow(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::request_authorization_code_handler request_authorization_code, slog::base_gate& gate); @@ -65,7 +65,7 @@ namespace nmos // fetch the bearer access token for the required scope(s) to access the protected APIs // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#requesting-a-token // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#accessing-protected-resources - // fetch the Token Issuer(authorization server)'s public keys fpr validating the incoming bearer access token + // fetch the Token Issuer(authorization server)'s public keys for validating the incoming bearer access token // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate); diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp index a969f0dab..22290985f 100644 --- a/Development/nmos/authorization_redirect_api.cpp +++ b/Development/nmos/authorization_redirect_api.cpp @@ -203,9 +203,8 @@ namespace nmos client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings)); }); - // The Authorization server may redirect error back due to something have went wrong - // such as resource owner rejects the request or the developer did something wrong - // when creating the Authorization request + // The authorization server may redirect an error back to this endpoint due to error conditions + // such as resource owner rejecting the request, or invalid authorization request { auto lock = authorization_state.write_lock(); // in order to update shared state try @@ -220,7 +219,7 @@ namespace nmos } catch (const authorization_flow_exception& e) { - slog::log(gate, SLOG_FLF) << "Authorization flow token request Authorization Flow error: " << utility::us2s(e.error.name) << " description: " << utility::us2s(e.description); + slog::log(gate, SLOG_FLF) << "Authorization flow token request authorization flow error: " << utility::us2s(e.error.name) << " description: " << utility::us2s(e.description); result = details::make_authorization_flow_error_response(status_codes::BadRequest, e.error.name, e.description); authorization_state.authorization_flow = authorization_state::failed; } diff --git a/Development/nmos/authorization_state.h b/Development/nmos/authorization_state.h index 9cf714198..2bd57db4d 100644 --- a/Development/nmos/authorization_state.h +++ b/Development/nmos/authorization_state.h @@ -39,7 +39,7 @@ namespace nmos struct authorization_state { - // mutex to be used to protect the members of the settings from simultaneous access by multiple threads + // mutex to be used to protect the members of the authorization_state from simultaneous access by multiple threads mutable nmos::mutex mutex; // authorization code flow settings @@ -64,7 +64,7 @@ namespace nmos // map of issuer (authorization server) to jwt_validator set for access token validation nmos::experimental::issuers issuers; - // the authorization server which is currently connected to + // currently connected authorization server web::uri authorization_server_uri; // OAuth 2.0 bearer token to access authorizaton protected APIs diff --git a/Development/nmos/authorization_utils.cpp b/Development/nmos/authorization_utils.cpp index 680c152c4..82b385d33 100644 --- a/Development/nmos/authorization_utils.cpp +++ b/Development/nmos/authorization_utils.cpp @@ -77,7 +77,7 @@ namespace nmos // get issuer version api_version version(const web::uri& issuer) { - // issuer uri should be like "https://server.example.com/{version} + // issuer uri should be of the form "https://server.example.com/{version}" api_version ver{ api_version{} }; if (!issuer.is_path_empty()) { diff --git a/Development/nmos/certificate_handlers.cpp b/Development/nmos/certificate_handlers.cpp index d91d70a27..ed52a73ab 100644 --- a/Development/nmos/certificate_handlers.cpp +++ b/Development/nmos/certificate_handlers.cpp @@ -123,7 +123,7 @@ namespace nmos } // construct callback to load RSA private keys from file based on settings, see nmos/certificate_settings.h - // require for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint + // required for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint load_rsa_private_keys_handler make_load_rsa_private_keys_handler(const nmos::settings& settings, slog::base_gate& gate) { // load the server private keys from files diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index 74df9b1bb..ec1d4bb4f 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -172,7 +172,7 @@ namespace nmos return config; } - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index cda94fa4d..6c16d7998 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -79,7 +79,7 @@ namespace nmos const auto& settings = model.settings; web::uri token_issuer; - // note: the ws_validate_authorization returns the token_issuer via function parameter + // note: ws_validate_authorization returns the token_issuer via function parameter const auto result = nmos::experimental::ws_validate_authorization(req, nmos::experimental::scopes::events, nmos::get_host_name(settings), token_issuer, access_token_validation, gate_); if (!result) { diff --git a/Development/nmos/jwk_utils.cpp b/Development/nmos/jwk_utils.cpp index 1e80ae144..9919616cd 100644 --- a/Development/nmos/jwk_utils.cpp +++ b/Development/nmos/jwk_utils.cpp @@ -124,7 +124,7 @@ namespace nmos using ssl::experimental::BIO_ptr; // supported Elliptic-Curve types - // see https://tools.ietf.org/search/rfc4492#appendix-A + // see https://tools.ietf.org/html/rfc4492#appendix-A const std::map curve = { { U("P-256"), NID_X9_62_prime256v1 }, diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp index cc09d630d..8d3499e98 100644 --- a/Development/nmos/jwt_validator_impl.cpp +++ b/Development/nmos/jwt_validator_impl.cpp @@ -107,7 +107,7 @@ namespace nmos } } - // reaching here, there must be because no matching public key to validate the access token + // reaching here indicates there is no matching public key to validate the access token // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it @@ -186,8 +186,8 @@ namespace nmos if (segments.size() >= aud_segments.size() && aud_segments.size()) { - // token audience got to be in wildcard domain name format, leftmost is a "*" charcater - // if not it is not going to match + // in order to match the token audience has to be in wildcard domain name format + // with a leftmost "*" character. // see https://tools.ietf.org/html/rfc4592#section-2.1.1 if (aud_segments[0] != "*") { @@ -219,7 +219,7 @@ namespace nmos } // scope optional - // If scope claim does not contain the expected scope, the Resource Server reject the token. + // If scope claim does not contain the expected scope, the Resource Server will reject the token. // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) { @@ -384,7 +384,7 @@ namespace nmos using namespace jwt::traits; auto decoded_token = jwt::decode(utility::us2s(token)); - // token does not guarantee to have client_id + // token is not guaranteed to have a client_id // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#client_id if (decoded_token.has_payload_claim("client_id")) { From 63fcbe76560796df3d26f713772896aafa659a3b Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 11:52:05 +0000 Subject: [PATCH 099/121] Remove unused jwt headers group --- Development/cmake/NmosCppLibraries.cmake | 2 -- 1 file changed, 2 deletions(-) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index f2fb6de76..08dea8c72 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -1066,7 +1066,6 @@ add_library( ${NMOS_CPP_CPPREST_HEADERS} ${NMOS_CPP_NMOS_SOURCES} ${NMOS_CPP_NMOS_HEADERS} - ${NMOS_CPP_JWT_HEADERS} ${NMOS_CPP_JWK_HEADERS} ${NMOS_CPP_PPLX_SOURCES} ${NMOS_CPP_PPLX_HEADERS} @@ -1088,7 +1087,6 @@ source_group("ssl\\Source Files" FILES ${NMOS_CPP_SSL_SOURCES}) source_group("bst\\Header Files" FILES ${NMOS_CPP_BST_HEADERS}) source_group("cpprest\\Header Files" FILES ${NMOS_CPP_CPPREST_HEADERS}) -source_group("jwt\\Header Files" FILES ${NMOS_CPP_JWT_HEADERS}) source_group("jwk\\Header Files" FILES ${NMOS_CPP_JWK_HEADERS}) source_group("nmos\\Header Files" FILES ${NMOS_CPP_NMOS_HEADERS}) source_group("pplx\\Header Files" FILES ${NMOS_CPP_PPLX_HEADERS}) From 046c7616e1950c73c61f1094dcee6e84ffbf5410 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 11:53:56 +0000 Subject: [PATCH 100/121] Set non-conan jwt-cpp build and install directories --- Development/cmake/NmosCppDependencies.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/cmake/NmosCppDependencies.cmake b/Development/cmake/NmosCppDependencies.cmake index 5dae81a18..d8df85da6 100644 --- a/Development/cmake/NmosCppDependencies.cmake +++ b/Development/cmake/NmosCppDependencies.cmake @@ -505,8 +505,8 @@ else() nmos-cpp::compile-settings ) target_include_directories(jwt-cpp PUBLIC - $ - $ + $ + $ ) endif() From 497eac1a63036f0f05552406f38fc541587d5bdf Mon Sep 17 00:00:00 2001 From: "Jonathan Thorpe (Sony)" Date: Thu, 14 Dec 2023 15:38:13 +0000 Subject: [PATCH 101/121] Add Windows 2022 and Ubuntu 22.04 targets without authorization. --- .github/workflows/src/amwa-test.yml | 2 +- .github/workflows/src/build-test.yml | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index f2e7aa06b..147deb980 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -20,7 +20,7 @@ cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = ${{ matrix.enable_authorization }}\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index d4e11bfa9..42db522da 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -22,8 +22,15 @@ jobs: use_conan: [true] force_cpprest_asio: [false] dns_sd_mode: [multicast, unicast] + enable_authorization: [False, True] exclude: # install_mdns is only meaningful on Linux + - os: macos-11 + enable_authorization: False + - os: windows-2019 + enable_authorization: False + - os: ubuntu-20.04 + enable_authorization: False - os: macos-11 install_mdns: true - os: windows-2019 @@ -38,17 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast + enable_authorization: False include: - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast + enable_authorization: True + - os: windows-2022 + install_mdns: false + use_conan: true + force_cpprest_asio: true + dns_sd_mode: multicast + enable_authorization: False + - os: ubuntu-22.04 + install_mdns: false + use_conan: true + force_cpprest_asio: false + dns_sd_mode: multicast + enable_authorization: True - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast + enable_authorization: False steps: - uses: actions/checkout@v3 From e8ca5d753a42d38569c5b433191ee9dfe1ec52ae Mon Sep 17 00:00:00 2001 From: "Jonathan Thorpe (Sony)" Date: Thu, 14 Dec 2023 15:41:19 +0000 Subject: [PATCH 102/121] Add authorization status to build target name --- .github/workflows/src/build-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 42db522da..5a5ec2483 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -12,7 +12,7 @@ env: SECRET_RESULTS_SHEET_ID: ${{ secrets.RESULTS_SHEET_ID }} jobs: build_and_test: - name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}})' + name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}}, enable_authorization: ${{ matrix.enable_authorization }})' runs-on: ${{ matrix.os }} strategy: fail-fast: false From 94646429eb3caff3c4f96819f43da97a000560fc Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 15:47:44 +0000 Subject: [PATCH 103/121] build the yml --- .github/workflows/build-test.yml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index efb7157fe..4c800e529 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,7 +12,7 @@ env: SECRET_RESULTS_SHEET_ID: ${{ secrets.RESULTS_SHEET_ID }} jobs: build_and_test: - name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}})' + name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}}, enable_authorization: ${{ matrix.enable_authorization }})' runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -22,8 +22,15 @@ jobs: use_conan: [true] force_cpprest_asio: [false] dns_sd_mode: [multicast, unicast] + enable_authorization: [False, True] exclude: # install_mdns is only meaningful on Linux + - os: macos-11 + enable_authorization: False + - os: windows-2019 + enable_authorization: False + - os: ubuntu-20.04 + enable_authorization: False - os: macos-11 install_mdns: true - os: windows-2019 @@ -38,17 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast + enable_authorization: False include: - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast + enable_authorization: True + - os: windows-2022 + install_mdns: false + use_conan: true + force_cpprest_asio: true + dns_sd_mode: multicast + enable_authorization: False + - os: ubuntu-22.04 + install_mdns: false + use_conan: true + force_cpprest_asio: false + dns_sd_mode: multicast + enable_authorization: True - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast + enable_authorization: False steps: - uses: actions/checkout@v3 @@ -345,7 +367,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = ${{ matrix.enable_authorization }}\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -874,7 +896,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = ${{ matrix.enable_authorization }}\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token From fa2141697ad6163fd3a52d761a81e647706eea43 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 15:52:56 +0000 Subject: [PATCH 104/121] Fix the formatting --- .github/workflows/build-test.yml | 18 +++++++++--------- .github/workflows/src/build-test.yml | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 4c800e529..f7a78e740 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -22,15 +22,15 @@ jobs: use_conan: [true] force_cpprest_asio: [false] dns_sd_mode: [multicast, unicast] - enable_authorization: [False, True] + enable_authorization: [False, True] exclude: # install_mdns is only meaningful on Linux - os: macos-11 - enable_authorization: False + enable_authorization: False - os: windows-2019 - enable_authorization: False + enable_authorization: False - os: ubuntu-20.04 - enable_authorization: False + enable_authorization: False - os: macos-11 install_mdns: true - os: windows-2019 @@ -45,32 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast - enable_authorization: False + enable_authorization: False include: - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: True + enable_authorization: True - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: False + enable_authorization: False - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: True + enable_authorization: True - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: False + enable_authorization: False steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 5a5ec2483..caf99bfb2 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -22,15 +22,15 @@ jobs: use_conan: [true] force_cpprest_asio: [false] dns_sd_mode: [multicast, unicast] - enable_authorization: [False, True] + enable_authorization: [False, True] exclude: # install_mdns is only meaningful on Linux - os: macos-11 - enable_authorization: False + enable_authorization: False - os: windows-2019 - enable_authorization: False + enable_authorization: False - os: ubuntu-20.04 - enable_authorization: False + enable_authorization: False - os: macos-11 install_mdns: true - os: windows-2019 @@ -45,32 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast - enable_authorization: False + enable_authorization: False include: - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: True + enable_authorization: True - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: False + enable_authorization: False - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: True + enable_authorization: True - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: False + enable_authorization: False steps: - uses: actions/checkout@v3 From 7f1eb5460a43c797de06150908029c437e2552cf Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 16:18:19 +0000 Subject: [PATCH 105/121] Fix the YML formatting --- .github/workflows/src/amwa-test.yml | 8 +++++++- .github/workflows/src/build-test.yml | 19 +++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 147deb980..8f611b21f 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -20,7 +20,7 @@ cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = ${{ matrix.enable_authorization }}\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -31,6 +31,12 @@ fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + if [[ "${{matrix.enable_authorization}}" ]]; then + printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py + else + printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py + fi + # Download testssl cd testssl curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index caf99bfb2..3e045c2c0 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -22,15 +22,15 @@ jobs: use_conan: [true] force_cpprest_asio: [false] dns_sd_mode: [multicast, unicast] - enable_authorization: [False, True] + enable_authorization: [false, true] exclude: # install_mdns is only meaningful on Linux - os: macos-11 - enable_authorization: False + enable_authorization: false - os: windows-2019 - enable_authorization: False + enable_authorization: false - os: ubuntu-20.04 - enable_authorization: False + enable_authorization: false - os: macos-11 install_mdns: true - os: windows-2019 @@ -45,32 +45,31 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast - enable_authorization: False include: - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: True + enable_authorization: true - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: False + enable_authorization: false - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: True + enable_authorization: true - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: False + enable_authorization: false steps: - uses: actions/checkout@v3 @@ -106,7 +105,7 @@ jobs: @import build-and-test build_and_test_ubuntu_14: - name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}})' + name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}}, enable_authorization: ${{ matrix.enable_authorization }})' runs-on: ubuntu-20.04 container: image: ubuntu:14.04 From 401e643b0fe18751c219a72057930ac0e85ed94a Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 16:32:32 +0000 Subject: [PATCH 106/121] Re-gen build-test.yml --- .github/workflows/build-test.yml | 36 ++++++++++++++++++---------- .github/workflows/src/build-test.yml | 1 + 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f7a78e740..6d13bbc1f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -22,15 +22,15 @@ jobs: use_conan: [true] force_cpprest_asio: [false] dns_sd_mode: [multicast, unicast] - enable_authorization: [False, True] + enable_authorization: [false, true] exclude: # install_mdns is only meaningful on Linux - os: macos-11 - enable_authorization: False + enable_authorization: false - os: windows-2019 - enable_authorization: False + enable_authorization: false - os: ubuntu-20.04 - enable_authorization: False + enable_authorization: false - os: macos-11 install_mdns: true - os: windows-2019 @@ -45,32 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast - enable_authorization: False + enable_authorization: true include: - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: True + enable_authorization: true - os: windows-2022 install_mdns: false use_conan: true force_cpprest_asio: true dns_sd_mode: multicast - enable_authorization: False + enable_authorization: false - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: True + enable_authorization: true - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast - enable_authorization: False + enable_authorization: false steps: - uses: actions/checkout@v3 @@ -367,7 +367,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = ${{ matrix.enable_authorization }}\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -378,6 +378,12 @@ jobs: fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + if [[ "${{matrix.enable_authorization}}" ]]; then + printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py + else + printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py + fi + # Download testssl cd testssl curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null @@ -564,7 +570,7 @@ jobs: build_and_test_ubuntu_14: - name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}})' + name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}}, enable_authorization: ${{ matrix.enable_authorization }})' runs-on: ubuntu-20.04 container: image: ubuntu:14.04 @@ -896,7 +902,7 @@ jobs: cd nmos-testing # Configure the Testing Tool so all APIs are tested with TLS and authorization - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.ENABLE_AUTH = ${{ matrix.enable_authorization }}\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py # Set the DNS-SD mode printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token @@ -907,6 +913,12 @@ jobs: fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + if [[ "${{matrix.enable_authorization}}" ]]; then + printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py + else + printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py + fi + # Download testssl cd testssl curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 3e045c2c0..007fbbd3d 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -45,6 +45,7 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast + enable_authorization: true include: - os: windows-2022 install_mdns: false From 0cfb811b635f5059a6951f25c5d3f89c5f13d1dd Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 17:07:06 +0000 Subject: [PATCH 107/121] Fix build-test.yml formatting --- .github/workflows/build-test.yml | 5 +++-- .github/workflows/src/amwa-test.yml | 2 +- .github/workflows/src/build-test.yml | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 6d13bbc1f..65086ef44 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -378,7 +378,7 @@ jobs: fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - if [[ "${{matrix.enable_authorization}}" ]]; then + if [[ "${{matrix.enable_authorization}}" == "true" ]]; then printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py else printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py @@ -582,6 +582,7 @@ jobs: use_conan: [false] force_cpprest_asio: [false] dns_sd_mode: [multicast] + enable_authorization: [true] steps: - uses: actions/checkout@v3 @@ -913,7 +914,7 @@ jobs: fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - if [[ "${{matrix.enable_authorization}}" ]]; then + if [[ "${{matrix.enable_authorization}}" == "true" ]]; then printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py else printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 8f611b21f..89396a152 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -31,7 +31,7 @@ fi printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py - if [[ "${{matrix.enable_authorization}}" ]]; then + if [[ "${{matrix.enable_authorization}}" == "true" ]]; then printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py else printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 007fbbd3d..24e6c0585 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -118,6 +118,7 @@ jobs: use_conan: [false] force_cpprest_asio: [false] dns_sd_mode: [multicast] + enable_authorization: [true] steps: - uses: actions/checkout@v3 From 10a84cdeadd1db93996533129abe8c5d6928c292 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 17:44:25 +0000 Subject: [PATCH 108/121] Fix running non-auth mode in run_nmos_testing script --- Sandbox/run_nmos_testing.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index d01d4e6d4..1114c3bc2 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -59,7 +59,9 @@ common_params=",\ \"host_address\":\"${host_ip}\",\ \"host_addresses\":[\"${host_ip}\"]\ " - +registry_params="" +node_params="" + if [[ "${config_secure}" == "True" ]]; then secure=true echo "Running TLS tests" @@ -114,10 +116,10 @@ if [[ "${config_auth}" == "True" ]]; then \"server_authorization\":true,\ \"service_unavailable_retry_after\":25\ " - registry_params=",\ + registry_params+=",\ \"label\":\"nmos-cpp-registry\"\ " - node_params=",\ + node_params+=",\ \"label\":\"nmos-cpp-node\",\ \"client_authorization\":true,\ \"authorization_flow\":\"client_credentials\",\ From fb8609e3d4bcc07f8cce2fb1028de7988bd1cd28 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 21:28:52 +0000 Subject: [PATCH 109/121] Config auth and non-auth nmos-cpp-node/registry with label --- Sandbox/run_nmos_testing.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 1114c3bc2..f22cf5804 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -59,8 +59,12 @@ common_params=",\ \"host_address\":\"${host_ip}\",\ \"host_addresses\":[\"${host_ip}\"]\ " -registry_params="" -node_params="" +registry_params=",\ + \"label\":\"nmos-cpp-registry\"\ + " +node_params=",\ + \"label\":\"nmos-cpp-node\"\ + " if [[ "${config_secure}" == "True" ]]; then secure=true @@ -116,11 +120,7 @@ if [[ "${config_auth}" == "True" ]]; then \"server_authorization\":true,\ \"service_unavailable_retry_after\":25\ " - registry_params+=",\ - \"label\":\"nmos-cpp-registry\"\ - " node_params+=",\ - \"label\":\"nmos-cpp-node\",\ \"client_authorization\":true,\ \"authorization_flow\":\"client_credentials\",\ \"authorization_scopes\":[\"registration\"],\ From d6614f1de433450256cd5d279d305ef494c4f3e3 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 14 Dec 2023 22:24:52 +0000 Subject: [PATCH 110/121] Fix nmos-cpp-node to run in non-auth mode --- Development/nmos/node_behaviour.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/nmos/node_behaviour.cpp b/Development/nmos/node_behaviour.cpp index ba86524e4..6b0c7a26b 100644 --- a/Development/nmos/node_behaviour.cpp +++ b/Development/nmos/node_behaviour.cpp @@ -760,7 +760,7 @@ namespace nmos grain.updated = strictly_increasing_update(resources); }); - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, get_authorization_bearer_token(), gate))); + registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, get_authorization_bearer_token ? get_authorization_bearer_token() : web::http::oauth2::experimental::oauth2_token{}, gate))); } events = web::json::value::array(); @@ -900,7 +900,7 @@ namespace nmos const auto registry_version = parse_api_version(web::uri::split_path(base_uri.path()).back()); if (registry_version != grain->version) break; - const auto& bearer_token = get_authorization_bearer_token(); + const auto bearer_token = get_authorization_bearer_token ? get_authorization_bearer_token() : web::http::oauth2::experimental::oauth2_token{}; registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, bearer_token, gate))); heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, bearer_token, gate))); From e0000947e0a0bb5271f124ea9a1eb6977a8e8a7b Mon Sep 17 00:00:00 2001 From: Simon Lo Date: Fri, 15 Dec 2023 09:22:20 +0000 Subject: [PATCH 111/121] Apply suggestions from code review Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- Development/nmos-cpp-node/main.cpp | 4 ++-- Development/nmos/settings.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 2391f9f21..2f1c791d3 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -115,7 +115,7 @@ int main(int argc, char* argv[]) } #endif -// only implement communication with Authorization server if IS-10 is required +// only implement communication with Authorization server if IS-10/BCP-003-02 is required // cf. preprocessor conditions in nmos::make_node_api, nmos::make_connection_api, nmos::make_events_api, nmos::make_channelmapping_api, make_events_ws_validate_handler nmos::experimental::authorization_state authorization_state; if (nmos::experimental::fields::server_authorization(node_model.settings)) @@ -153,7 +153,7 @@ int main(int argc, char* argv[]) } #endif -// only implement communication with Authorization server if IS-10 is required +// only implement communication with Authorization server if IS-10/BCP-003-02 is required if (nmos::experimental::fields::client_authorization(node_model.settings)) { std::map api_routers; diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index e69644e19..3f329c095 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -410,7 +410,7 @@ namespace nmos // authorization_flow [node]: used to specify the authorization flow for the registered scopes // supported flow are authorization_code and client_credentials - // client_credentials SHOULD only be used for NO user interface node/registry, otherwise authorization_code MUST be used + // client_credentials SHOULD only be used when the node/registry has NO user interface, otherwise authorization_code MUST be used const web::json::field_as_string_or authorization_flow{ U("authorization_flow"), U("authorization_code") }; // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration From 771948f30f34e6b0d34d16f06bdc27943393cc62 Mon Sep 17 00:00:00 2001 From: Simon Lo Date: Fri, 15 Dec 2023 09:24:16 +0000 Subject: [PATCH 112/121] Apply suggestions from code review Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- Development/nmos-cpp-node/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 2f1c791d3..1d6c90ae9 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -214,7 +214,7 @@ int main(int argc, char* argv[]) } } -// only implement communication with Authorization server if IS-10 is required +// only implement communication with Authorization server if IS-10/BCP-003-02 is required if (nmos::experimental::fields::client_authorization(node_model.settings) || nmos::experimental::fields::server_authorization(node_model.settings)) { // IS-10 client registration, fetch access token, and fetch authorization server token public key From d22b02f4b1ed25b6b8e0b40feb8b84bbef1b8df3 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 10:42:10 +0000 Subject: [PATCH 113/121] Update comments on IS-10 support --- Development/nmos-cpp-node/config.json | 3 ++- Development/nmos-cpp-registry/main.cpp | 4 ++-- Development/nmos/settings.h | 2 ++ README.md | 25 ++++++++++++++----------- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 4aaaa5823..3a2e39501 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -342,10 +342,11 @@ // authorization_scopes [node]: used to specify the supported scopes for client registration // supported scopes are registration, query, node, connection, events and channelmapping - //"authorization_scopes": array-of-scope-string, + //"authorization_scopes": [ "registration" ], // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint // supported methods are none, client_secret_basic and private_key_jwt, default to client_secret_basic, where none is used for public client + // when using private_key_jwt, the JWT is created and signed by the node's private key //"token_endpoint_auth_method": "client_secret_basic", // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index 01a1f9bcf..6e429b7c0 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -111,7 +111,7 @@ int main(int argc, char* argv[]) } #endif -// only implement communication with Authorization server if IS-10 is required +// only implement communication with Authorization server if IS-10/BCP-003-02 is required // cf. preprocessor conditions in nmos::make_registration_api, nmos::make_query_api, make_query_ws_validate_handler nmos::experimental::authorization_state authorization_state; if (nmos::experimental::fields::server_authorization(registry_model.settings)) @@ -148,7 +148,7 @@ int main(int argc, char* argv[]) } #endif -// only implement communication with Authorization server if IS-10 is required +// only implement communication with Authorization server if IS-10/BCP-003-02 is required if (nmos::experimental::fields::server_authorization(registry_model.settings)) { auto load_ca_certificates = registry_implementation.load_ca_certificates; diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 3f329c095..d17697d93 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -426,6 +426,7 @@ namespace nmos // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint // supported methods are client_secret_basic and private_key_jwt, default to client_secret_basic + // when using private_key_jwt, the JWT is created and signed by the node's private key const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic")}; // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration @@ -445,6 +446,7 @@ namespace nmos // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys const web::json::field_as_integer_or service_unavailable_retry_after{ U("service_unavailable_retry_after"), 5}; } } diff --git a/README.md b/README.md index 4a2653c09..09605faba 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) - [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) +- [AMWA BCP-003-02 Authorization in NMOS Systems](https://specs.amwa.tv/bcp-003-02/) - [AMWA BCP-004-01 NMOS Receiver Capabilities](https://specs.amwa.tv/bcp-004-01/) - [AMWA BCP-006-01 NMOS With JPEG XS](https://specs.amwa.tv/bcp-006-01/) @@ -55,16 +56,18 @@ Several vendors have deployed JT-NM Tested badged products, using nmos-cpp, to t The following configurations, defined by the [build-test](.github/workflows/src/build-test.yml) jobs, are built and unit tested automatically via continuous integration. -| Platform | Version | Build Options | Test Options | -|----------|---------------------------|------------------------------------|--------------------------------------------| -| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
Unicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
Multicast DNS-SD | -| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
Multicast DNS-SD | -| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | -| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
Multicast DNS-SD | +| Platform | Version | Build Options | Test Options | +|----------|---------------------------|------------------------------------|--------------------------------------------------------------------| +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
Multicast DNS-SD | +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Unicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
IS-10 Authorization
Multicast DNS-SD | The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is automatically run against the APIs of the **nmos-cpp-node** and **nmos-cpp-registry** applications. @@ -113,7 +116,7 @@ The implementation is designed to be extended. Development is ongoing, following Recent activity on the project (newest first): -- Added support for IS-10 Autorization API +- Added support for IS-10 Authorization API - Added support for HSTS and OCSP stapling - Added support for BCP-006-01 v1.0-dev, which can be demonstrated with **nmos-cpp-node** by using `"video_type": "video/jxsv"` - Updates to the GitHub Actions build-test workflow for better coverage of platforms and to include unicast DNS-SD tests From 490edbc242ef082ed70dbca753bfd8b7037cd20e Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 10:51:36 +0000 Subject: [PATCH 114/121] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09605faba..1ff32210c 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ The implementation is designed to be extended. Development is ongoing, following Recent activity on the project (newest first): -- Added support for IS-10 Authorization API +- Added support for IS-10 Authorization - Added support for HSTS and OCSP stapling - Added support for BCP-006-01 v1.0-dev, which can be demonstrated with **nmos-cpp-node** by using `"video_type": "video/jxsv"` - Updates to the GitHub Actions build-test workflow for better coverage of platforms and to include unicast DNS-SD tests From feb6c36830e5ed0413142b02f5bea4177b6bbd08 Mon Sep 17 00:00:00 2001 From: "Jonathan Thorpe (Sony)" Date: Fri, 15 Dec 2023 11:07:37 +0000 Subject: [PATCH 115/121] Add auth mode to artifact directory name --- .github/workflows/src/build-test.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 24e6c0585..4f35c2cbd 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -78,16 +78,22 @@ jobs: - name: set environment variables shell: bash run: | + if [[ "${{ matrix.enable_authorization }}" == "true" ]]; then + authorization_mode=auth + else + authorization_mode=noauth + fi + if [[ "${{ runner.os }}" == "Linux" ]]; then if [[ "${{ matrix.install_mdns }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV fi elif [[ "${{ matrix.force_cpprest_asio }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_asio" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_asio_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_auth_$authorization_mode" >> $GITHUB_ENV fi GITHUB_COMMIT=`echo "${{ github.sha }}" | cut -c1-7` echo "GITHUB_COMMIT=$GITHUB_COMMIT" >> $GITHUB_ENV From 6a9c386f074357bd6677c67d6c22be9facd173a1 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 11:14:34 +0000 Subject: [PATCH 116/121] Update Readme --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1ff32210c..40283ad40 100644 --- a/README.md +++ b/README.md @@ -56,18 +56,18 @@ Several vendors have deployed JT-NM Tested badged products, using nmos-cpp, to t The following configurations, defined by the [build-test](.github/workflows/src/build-test.yml) jobs, are built and unit tested automatically via continuous integration. -| Platform | Version | Build Options | Test Options | -|----------|---------------------------|------------------------------------|--------------------------------------------------------------------| -| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Unicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | -| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Platform | Version | Build Options | Test Options | +|----------|----------------------------------------------|------------------------------------|--------------------------------------------------------------------| +| Linux | Ubuntu 22.04 (GCC 11.2.0) (No Authorization) | Avahi | Secure Communications
Multicast DNS-SD | +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Unicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) (No Authorization) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
IS-10 Authorization
Multicast DNS-SD | The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is automatically run against the APIs of the **nmos-cpp-node** and **nmos-cpp-registry** applications. From 73a6c74a17076ac518ceb3b0a57f7a40cc38fe1e Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 11:15:15 +0000 Subject: [PATCH 117/121] Fix output result --- .github/workflows/build-test.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 65086ef44..ddc4b134d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -78,16 +78,22 @@ jobs: - name: set environment variables shell: bash run: | + if [[ "${{ matrix.enable_authorization }}" == "true" ]]; then + authorization_mode=auth + else + authorization_mode=noauth + fi + if [[ "${{ runner.os }}" == "Linux" ]]; then if [[ "${{ matrix.install_mdns }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV fi elif [[ "${{ matrix.force_cpprest_asio }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_asio" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_asio_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_auth_$authorization_mode" >> $GITHUB_ENV fi GITHUB_COMMIT=`echo "${{ github.sha }}" | cut -c1-7` echo "GITHUB_COMMIT=$GITHUB_COMMIT" >> $GITHUB_ENV From f8549930f8d2bcf94e21a93d0c093c0754d884b4 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 11:23:53 +0000 Subject: [PATCH 118/121] Update readme --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 40283ad40..1ff32210c 100644 --- a/README.md +++ b/README.md @@ -56,18 +56,18 @@ Several vendors have deployed JT-NM Tested badged products, using nmos-cpp, to t The following configurations, defined by the [build-test](.github/workflows/src/build-test.yml) jobs, are built and unit tested automatically via continuous integration. -| Platform | Version | Build Options | Test Options | -|----------|----------------------------------------------|------------------------------------|--------------------------------------------------------------------| -| Linux | Ubuntu 22.04 (GCC 11.2.0) (No Authorization) | Avahi | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Unicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| Windows | Server 2022 (VS 2022) (No Authorization) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | -| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
IS-10 Authorization
Multicast DNS-SD | -| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Platform | Version | Build Options | Test Options | +|----------|---------------------------|------------------------------------|--------------------------------------------------------------------| +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
Multicast DNS-SD | +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Unicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
IS-10 Authorization
Multicast DNS-SD | The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is automatically run against the APIs of the **nmos-cpp-node** and **nmos-cpp-registry** applications. From 0fbf7871bfc95dd447a08ffc3bf092a224716703 Mon Sep 17 00:00:00 2001 From: Simon Lo Date: Fri, 15 Dec 2023 12:37:05 +0000 Subject: [PATCH 119/121] Update Development/nmos-cpp-node/config.json Co-authored-by: jonathan-r-thorpe <64410119+jonathan-r-thorpe@users.noreply.github.com> --- Development/nmos-cpp-node/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 3a2e39501..c3367a9d4 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -325,7 +325,7 @@ // server_authorization [registry, node]: whether server should use authorization to protect its APIs //"server_authorization": false, - // authorization_code_flow_max [node]: timeout for the authorization code workflow (in seconds) + // authorization_code_flow_max [node]: timeout for the authorization code flow (in seconds) // No timeout if value is set to -1, default to 30 seconds //"authorization_code_flow_max": 30, From 6fcd5690f37cd4c2e9b72f25e91b99b7aa2d8236 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 13:05:34 +0000 Subject: [PATCH 120/121] Remove summary tag to match with nmos-cpp comment style --- Development/cpprest/basic_utils.h | 16 ++--- Development/pplx/pplx_utils.h | 106 ++++++++---------------------- 2 files changed, 35 insertions(+), 87 deletions(-) diff --git a/Development/cpprest/basic_utils.h b/Development/cpprest/basic_utils.h index 01aa76f57..ae60371f5 100644 --- a/Development/cpprest/basic_utils.h +++ b/Development/cpprest/basic_utils.h @@ -31,11 +31,9 @@ namespace utility } } - /// - /// Encode the given byte array into a base64url string - /// using the alternative alphabet and skipping the padding - /// as per https://tools.ietf.org/html/rfc4648#section-5 - /// + // Encode the given byte array into a base64url string + // using the alternative alphabet and skipping the padding + // as per https://tools.ietf.org/html/rfc4648#section-5 inline utility::string_t to_base64url(const std::vector& data) { auto str = utility::conversions::to_base64(data); @@ -51,11 +49,9 @@ namespace utility return str; } - /// - /// Decode the given base64url string to a byte array - /// using the alternative alphabet and skipping the padding - /// as per https://tools.ietf.org/html/rfc4648#section-5 - /// + // Decode the given base64url string to a byte array + // using the alternative alphabet and skipping the padding + // as per https://tools.ietf.org/html/rfc4648#section-5 inline std::vector from_base64url(utility::string_t str) { for (auto& c : str) diff --git a/Development/pplx/pplx_utils.h b/Development/pplx/pplx_utils.h index 3b90a0ce3..fd20aa2f5 100644 --- a/Development/pplx/pplx_utils.h +++ b/Development/pplx/pplx_utils.h @@ -11,32 +11,16 @@ namespace Concurrency // since namespace pplx = Concurrency namespace pplx #endif { - /// - /// Creates a task that completes after a specified amount of time. - /// - /// - /// The number of milliseconds after which the task should complete. - /// - /// - /// Cancellation token for cancellation of this operation. - /// - /// - /// Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. - /// + // Creates a task that completes after a specified amount of time. + // milliseconds: The number of milliseconds after which the task should complete. + // token: Cancellation token for cancellation of this operation. + // Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. pplx::task complete_after(unsigned int milliseconds, const pplx::cancellation_token& token = pplx::cancellation_token::none()); - /// - /// Creates a task that completes after a specified amount of time. - /// - /// - /// The amount of time (milliseconds and up) after which the task should complete. - /// - /// - /// Cancellation token for cancellation of this operation. - /// - /// - /// Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. - /// + // Creates a task that completes after a specified amount of time. + // duration: The amount of time (milliseconds and up) after which the task should complete. + // token: Cancellation token for cancellation of this operation. + // Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. template inline pplx::task complete_after(const std::chrono::duration& duration, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { @@ -45,42 +29,24 @@ namespace pplx : pplx::task_from_result(); } - /// - /// Creates a task that completes at a specified time. - /// - /// - /// The time point at which the task should complete. - /// - /// - /// Cancellation token for cancellation of this operation. - /// - /// - /// Because the scheduler is cooperative in nature, the time at which the task completes could be after the specified time. - /// + // Creates a task that completes at a specified time. + // time: The time point at which the task should complete. + // token: Cancellation token for cancellation of this operation. + // Because the scheduler is cooperative in nature, the time at which the task completes could be after the specified time. template inline pplx::task complete_at(const std::chrono::time_point& time, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { return complete_after(time - Clock::now(), token); } - /// - /// Creates a task for an asynchronous do-while loop. Executes a task repeatedly, until the returned condition value becomes false. - /// - /// - /// This function should create a task that performs the loop iteration and returns the Boolean value of the loop condition. - /// - /// - /// Cancellation token for cancellation of the do-while loop. - /// + // Creates a task for an asynchronous do-while loop. Executes a task repeatedly, until the returned condition value becomes false. + // create_iteration_task: This function should create a task that performs the loop iteration and returns the Boolean value of the loop condition. + // token: Cancellation token for cancellation of the do-while loop. pplx::task do_while(const std::function()>& create_iteration_task, const pplx::cancellation_token& token = pplx::cancellation_token::none()); - /// - /// Returns true if the task is default constructed. - /// - /// - /// A default constructed task cannot be used until you assign a valid task to it. Methods such as get, wait or then - /// will throw an invalid_argument exception when called on a default constructed task. - /// + // Returns true if the task is default constructed. + // A default constructed task cannot be used until you assign a valid task to it. Methods such as get, wait or then + // will throw an invalid_argument exception when called on a default constructed task. template bool empty(const pplx::task& task) { @@ -99,7 +65,7 @@ namespace pplx catch (...) {} } } - + struct exception_observer { template @@ -109,13 +75,9 @@ namespace pplx } }; - /// - /// Silently 'observe' any exception thrown from a task. - /// - /// - /// Exceptions that are unobserved when a task is destructed will terminate the process. - /// Add this as a continuation to silently swallow all exceptions. - /// + // Silently 'observe' any exception thrown from a task. + // Exceptions that are unobserved when a task is destructed will terminate the process. + // Add this as a continuation to silently swallow all exceptions. inline exception_observer observe_exception() { return exception_observer(); @@ -153,26 +115,18 @@ namespace pplx std::vector> tasks; }; - /// - /// Silently 'observe' all exceptions thrown from a range of tasks. - /// - /// - /// Exceptions that are unobserved when a task is destructed will terminate the process. - /// Add this as a continuation to silently swallow all exceptions. - /// + // Silently 'observe' all exceptions thrown from a range of tasks. + // Exceptions that are unobserved when a task is destructed will terminate the process. + // Add this as a continuation to silently swallow all exceptions. template ().begin())>::value_type::result_type> inline exceptions_observer observe_exceptions(InputRange&& tasks) { return exceptions_observer(std::forward(tasks)); } - /// - /// Silently 'observe' all exceptions thrown from a range of tasks. - /// - /// - /// Exceptions that are unobserved when a task is destructed will terminate the process. - /// Add this as a continuation to silently swallow all exceptions. - /// + // Silently 'observe' all exceptions thrown from a range of tasks. + // Exceptions that are unobserved when a task is destructed will terminate the process. + // Add this as a continuation to silently swallow all exceptions. template ::value_type::result_type> inline exceptions_observer observe_exceptions(InputIterator&& first, InputIterator&& last) { @@ -244,9 +198,7 @@ namespace pplx } } - /// - /// RAII helper for classes that have asynchronous open/close member functions. - /// + // RAII helper for classes that have asynchronous open/close member functions. template struct open_close_guard { From b900da204d6e4d0ece528ad655b6505581134039 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Fri, 15 Dec 2023 13:06:56 +0000 Subject: [PATCH 121/121] Bump up jwt-cpp to v0.7.0 --- Development/conanfile.txt | 2 +- Development/third_party/jwt-cpp/README.md | 4 +- Development/third_party/jwt-cpp/base.h | 9 +- Development/third_party/jwt-cpp/jwt.h | 130 +++++++++++++++------- 4 files changed, 97 insertions(+), 48 deletions(-) diff --git a/Development/conanfile.txt b/Development/conanfile.txt index 01f151bf1..08e098705 100644 --- a/Development/conanfile.txt +++ b/Development/conanfile.txt @@ -6,7 +6,7 @@ openssl/1.1.1s json-schema-validator/2.2.0 nlohmann_json/3.11.2 zlib/1.2.13 -jwt-cpp/0.6.0 +jwt-cpp/0.7.0 [imports] bin, *.dll -> ./bin diff --git a/Development/third_party/jwt-cpp/README.md b/Development/third_party/jwt-cpp/README.md index 93fb1ce2b..9aea7c85e 100644 --- a/Development/third_party/jwt-cpp/README.md +++ b/Development/third_party/jwt-cpp/README.md @@ -68,7 +68,7 @@ jwt::basic_claim claim(json::object({{"json", t This allows for complete freedom when picking which libraries you want to use. For more information, [read this page](docs/traits.md)). -For your convience there are serval traits implementation which provide some popular JSON libraries. They are: +For your convenience there are serval traits implementation which provide some popular JSON libraries. They are: [![picojson][picojson]](https://github.com/kazuho/picojson) [![nlohmann][nlohmann]](https://github.com/nlohmann/json) @@ -82,7 +82,7 @@ For your convience there are serval traits implementation which provide some pop In order to maintain compatibility, [picojson](https://github.com/kazuho/picojson) is still used to provide a specialized `jwt::claim` along with all helpers. Defining `JWT_DISABLE_PICOJSON` will remove this optional dependency. It's possible to directly include the traits defaults for the other JSON libraries. See the [traits examples](https://github.com/Thalhammer/jwt-cpp/tree/master/example/traits) for details. -As for the base64 requirements of JWTs, this libary provides `base.h` with all the required implentation; However base64 implementations are very common, with varying degrees of performance. When providing your own base64 implementation, you can define `JWT_DISABLE_BASE64` to remove the jwt-cpp implementation. +As for the base64 requirements of JWTs, this library provides `base.h` with all the required implementation; However base64 implementations are very common, with varying degrees of performance. When providing your own base64 implementation, you can define `JWT_DISABLE_BASE64` to remove the jwt-cpp implementation. ### Getting Started diff --git a/Development/third_party/jwt-cpp/base.h b/Development/third_party/jwt-cpp/base.h index cef493d19..fd3ee281b 100644 --- a/Development/third_party/jwt-cpp/base.h +++ b/Development/third_party/jwt-cpp/base.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -38,7 +39,7 @@ namespace jwt { return data; } static const std::string& fill() { - static std::string fill{"="}; + static const std::string fill{"="}; return fill; } }; @@ -61,7 +62,7 @@ namespace jwt { return data; } static const std::string& fill() { - static std::string fill{"%3d"}; + static const std::string fill{"%3d"}; return fill; } }; @@ -81,8 +82,8 @@ namespace jwt { 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; return data; } - static const std::initializer_list& fill() { - static std::initializer_list fill{"%3D", "%3d"}; + static const std::vector& fill() { + static const std::vector fill{"%3D", "%3d"}; return fill; } }; diff --git a/Development/third_party/jwt-cpp/jwt.h b/Development/third_party/jwt-cpp/jwt.h index ec63b64c7..b2b998a2e 100644 --- a/Development/third_party/jwt-cpp/jwt.h +++ b/Development/third_party/jwt-cpp/jwt.h @@ -23,7 +23,9 @@ #include #include +#include #include +#include #include #include #include @@ -492,7 +494,7 @@ namespace jwt { * * \param certstr String containing the certificate encoded as pem * \param pw Password used to decrypt certificate (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error ocurred) + * \param ec error_code for error_detection (gets cleared if no error occurred) */ inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw, std::error_code& ec) { @@ -543,28 +545,18 @@ namespace jwt { } /** - * \brief Convert the certificate provided as base64 DER to PEM. - * - * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info - * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * \brief Convert the certificate provided as DER to PEM. * - * \tparam Decode is callabled, taking a string_type and returns a string_type. - * It should ensure the padding of the input and then base64 decode and return - * the results. - * - * \param cert_base64_der_str String containing the certificate encoded as base64 DER - * \param decode The function to decode the cert - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param cert_der_str String containing the certificate encoded as base64 DER + * \param ec error_code for error_detection (gets cleared if no error occurs) */ - template - std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, Decode decode, - std::error_code& ec) { + inline std::string convert_der_to_pem(const std::string& cert_der_str, std::error_code& ec) { ec.clear(); - const auto decodedStr = decode(cert_base64_der_str); - auto c_str = reinterpret_cast(decodedStr.c_str()); + + auto c_str = reinterpret_cast(cert_der_str.c_str()); std::unique_ptr cert( - d2i_X509(NULL, &c_str, static_cast(decodedStr.size())), X509_free); + d2i_X509(NULL, &c_str, static_cast(cert_der_str.size())), X509_free); auto certbio = make_mem_buf_bio(); if (!cert || !certbio) { ec = error::rsa_error::create_mem_bio_failed; @@ -586,6 +578,28 @@ namespace jwt { return {ptr, static_cast(len)}; } + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64 decode and return + * the results. + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param decode The function to decode the cert + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + template + std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, Decode decode, + std::error_code& ec) { + ec.clear(); + const auto decoded_str = decode(cert_base64_der_str); + return convert_der_to_pem(decoded_str, ec); + } + /** * \brief Convert the certificate provided as base64 DER to PEM. * @@ -607,6 +621,21 @@ namespace jwt { error::throw_if_error(ec); return res; } + + /** + * \brief Convert the certificate provided as DER to PEM. + * + * \param cert_der_str String containing the DER certificate + * \param decode The function to decode the cert + * \throw rsa_exception if an error occurred + */ + inline std::string convert_der_to_pem(const std::string& cert_der_str) { + std::error_code ec; + auto res = convert_der_to_pem(cert_der_str, ec); + error::throw_if_error(ec); + return res; + } + #ifndef JWT_DISABLE_BASE64 /** * \brief Convert the certificate provided as base64 DER to PEM. @@ -615,7 +644,7 @@ namespace jwt { * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] * * \param cert_base64_der_str String containing the certificate encoded as base64 DER - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error occurs) */ inline std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, std::error_code& ec) { auto decode = [](const std::string& token) { @@ -647,7 +676,7 @@ namespace jwt { * * \param key String containing the certificate encoded as pem * \param password Password used to decrypt certificate (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error occurs) */ inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { @@ -701,7 +730,7 @@ namespace jwt { * * \param key String containing a private key as pem * \param password Password used to decrypt key (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error occurs) */ inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { @@ -742,7 +771,7 @@ namespace jwt { * * \param key String containing the certificate encoded as pem * \param password Password used to decrypt certificate (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error occurs) */ inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { @@ -797,7 +826,7 @@ namespace jwt { * * \param key String containing a private key as pem * \param password Password used to decrypt key (leave empty if not encrypted) - * \param ec error_code for error_detection (gets cleared if no error occures) + * \param ec error_code for error_detection (gets cleared if no error occurs) */ inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { @@ -1944,7 +1973,7 @@ namespace jwt { // TODO(prince-chrismc): I am not convienced this is meaningful anymore static_assert( value, - "object_type must implementate the subscription operator '[]' taking string_type as an arguement"); + "object_type must implementate the subscription operator '[]' taking string_type as an argument"); }; template @@ -2120,14 +2149,14 @@ namespace jwt { /** * Serialize claim to output stream from wrapped JSON value - * \return ouput stream + * \return output stream */ std::ostream& operator<<(std::ostream& os) { return os << val; } /** * Get type of contained JSON value * \return Type - * \throw std::logic_error An internal error occured + * \throw std::logic_error An internal error occurred */ json::type get_type() const { return json_traits::get_type(val); } @@ -2399,7 +2428,7 @@ namespace jwt { public: using basic_claim_t = basic_claim; /** - * Check if algortihm is present ("alg") + * Check if algorithm is present ("alg") * \return true if present, false otherwise */ bool has_algorithm() const noexcept { return has_header_claim("alg"); } @@ -2469,7 +2498,7 @@ namespace jwt { template class decoded_jwt : public header, public payload { protected: - /// Unmodifed token, as passed to constructor + /// Unmodified token, as passed to constructor typename json_traits::string_type token; /// Header part decoded from base64 typename json_traits::string_type header; @@ -2843,7 +2872,7 @@ namespace jwt { // The configured default leeway for this verification size_t default_leeway{0}; - // The claim key to apply this comparision on + // The claim key to apply this comparison on typename json_traits::string_type claim_key{}; // Helper method to get a claim from the jwt in this context @@ -2989,18 +3018,37 @@ namespace jwt { } static std::string to_lower_unicode(const std::string& str, const std::locale& loc) { -#if __cplusplus > 201103L - std::wstring_convert, wchar_t> conv; - auto wide = conv.from_bytes(str); + std::mbstate_t state = std::mbstate_t(); + const char* in_next = str.data(); + const char* in_end = str.data() + str.size(); + std::wstring wide; + wide.reserve(str.size()); + + while (in_next != in_end) { + wchar_t wc; + std::size_t result = std::mbrtowc(&wc, in_next, in_end - in_next, &state); + if (result == static_cast(-1)) { + throw std::runtime_error("encoding error: " + std::string(std::strerror(errno))); + } else if (result == static_cast(-2)) { + throw std::runtime_error("conversion error: next bytes constitute an incomplete, but so far " + "valid, multibyte character."); + } + in_next += result; + wide.push_back(wc); + } + auto& f = std::use_facet>(loc); f.tolower(&wide[0], &wide[0] + wide.size()); - return conv.to_bytes(wide); -#else - std::string result; - std::transform(str.begin(), str.end(), std::back_inserter(result), - [&loc](unsigned char c) { return std::tolower(c, loc); }); - return result; -#endif + + std::string out; + out.reserve(wide.size()); + for (wchar_t wc : wide) { + char mb[MB_LEN_MAX]; + std::size_t n = std::wcrtomb(mb, wc, &state); + if (n != static_cast(-1)) out.append(mb, n); + } + + return out; } }; } // namespace verify_ops @@ -3124,7 +3172,7 @@ namespace jwt { * Check is casesensitive. * * \param type Type Header Parameter to check for. - * \param locale Localization functionality to use when comapring + * \param locale Localization functionality to use when comparing * \return *this to allow chaining */ verifier& with_type(const typename json_traits::string_type& type, std::locale locale = std::locale{}) { @@ -3385,7 +3433,7 @@ namespace jwt { bool has_key_operations() const noexcept { return has_jwk_claim("key_ops"); } /** - * Check if algortihm is present ("alg") + * Check if algorithm is present ("alg") * \return true if present, false otherwise */ bool has_algorithm() const noexcept { return has_jwk_claim("alg"); }