From 582e3523cccac08a3c864e22366de0fd467dd03a Mon Sep 17 00:00:00 2001 From: Rustam Gamidov Date: Fri, 21 Aug 2026 14:07:41 +0300 Subject: [PATCH] Add MtlsTokenProvider for mTLS authentication Implementing auto refreshing in place not using AutoRefreshToken and TokenEndpoint for simplicity and because mtls endpoint has very simple list of arguments and some features implemented in client authentication may be not supported at the moment. Relates-To: DATASDK-105 Signed-off-by: Rustam Gamidov --- examples/CMakeLists.txt | 16 +- examples/MtlsAuthenticationExample.cpp | 169 +++++++ examples/MtlsAuthenticationExample.h | 42 ++ examples/Options.h | 27 +- examples/main.cpp | 75 ++- .../olp/authentication/AuthenticationClient.h | 19 +- .../olp/authentication/MtlsProperties.h | 70 +++ .../include/olp/authentication/MtlsSettings.h | 70 +++ .../olp/authentication/MtlsTokenProvider.h | 158 ++++++ .../src/AuthenticationClient.cpp | 7 +- .../src/AuthenticationClientImpl.cpp | 97 ++++ .../src/AuthenticationClientImpl.h | 11 +- .../src/MtlsTokenProvider.cpp | 189 ++++++++ .../src/MtlsTokenProviderPrivate.h | 72 +++ .../tests/AuthenticationClientTest.cpp | 259 ++++++++-- .../common/AuthenticationClientImplTestable.h | 68 +++ .../AuthenticationMockedResponses.h | 2 +- tests/common/CMakeLists.txt | 2 + .../TokenProviderTest.cpp | 457 +++++++++++++++++- 19 files changed, 1742 insertions(+), 68 deletions(-) create mode 100644 examples/MtlsAuthenticationExample.cpp create mode 100644 examples/MtlsAuthenticationExample.h create mode 100644 olp-cpp-sdk-authentication/include/olp/authentication/MtlsProperties.h create mode 100644 olp-cpp-sdk-authentication/include/olp/authentication/MtlsSettings.h create mode 100644 olp-cpp-sdk-authentication/include/olp/authentication/MtlsTokenProvider.h create mode 100644 olp-cpp-sdk-authentication/src/MtlsTokenProvider.cpp create mode 100644 olp-cpp-sdk-authentication/src/MtlsTokenProviderPrivate.h create mode 100644 tests/common/AuthenticationClientImplTestable.h rename tests/{integration/olp-cpp-sdk-authentication => common}/AuthenticationMockedResponses.h (99%) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 2a0684c91..39078a0e2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -20,6 +20,7 @@ set(OLP_SDK_DATASERVICE_READ_EXAMPLE_TARGET dataservice-read-example) set(OLP_SDK_DATASERVICE_WRITE_EXAMPLE_TARGET dataservice-write-example) set(OLP_SDK_DATASERVICE_CACHE_EXAMPLE_TARGET dataservice-cache-example) set(OLP_SDK_DATASERVICE_READ_STREAM_LAYER_EXAMPLE_TARGET dataservice-read-stream-layer-example) +set(OLP_SDK_MTLS_AUTHENTICATION_EXAMPLE_TARGET mtls-authentication-example) set(OLP_SDK_EXAMPLE_SUCCESS_STRING "Example has finished successfully") set(OLP_SDK_EXAMPLE_FAILURE_STRING "Example failed!") @@ -81,6 +82,14 @@ else() olp-cpp-sdk-authentication olp-cpp-sdk-dataservice-read) + add_library(${OLP_SDK_MTLS_AUTHENTICATION_EXAMPLE_TARGET} + ./MtlsAuthenticationExample.cpp + ./Examples.h + ./MtlsAuthenticationExample.h) + + target_link_libraries(${OLP_SDK_MTLS_AUTHENTICATION_EXAMPLE_TARGET} + olp-cpp-sdk-authentication) + target_compile_definitions(${OLP_SDK_DATASERVICE_READ_EXAMPLE_TARGET} PRIVATE EXAMPLES_LIBRARY) target_compile_definitions(${OLP_SDK_DATASERVICE_WRITE_EXAMPLE_TARGET} @@ -89,6 +98,8 @@ else() PRIVATE EXAMPLES_LIBRARY) target_compile_definitions(${OLP_SDK_DATASERVICE_READ_STREAM_LAYER_EXAMPLE_TARGET} PRIVATE EXAMPLES_LIBRARY) + target_compile_definitions(${OLP_SDK_MTLS_AUTHENTICATION_EXAMPLE_TARGET} + PRIVATE EXAMPLES_LIBRARY) if(BUILD_SHARED_LIBS) target_compile_definitions(${OLP_SDK_DATASERVICE_READ_EXAMPLE_TARGET} PUBLIC EXAMPLES_SHARED_LIBRARY) @@ -98,6 +109,8 @@ else() PUBLIC EXAMPLES_SHARED_LIBRARY) target_compile_definitions(${OLP_SDK_DATASERVICE_READ_STREAM_LAYER_EXAMPLE_TARGET} PUBLIC EXAMPLES_SHARED_LIBRARY) + target_compile_definitions(${OLP_SDK_MTLS_AUTHENTICATION_EXAMPLE_TARGET} + PUBLIC EXAMPLES_SHARED_LIBRARY) endif() add_executable(${OLP_SDK_DATASERVICE_EXAMPLE_TARGET} @@ -109,6 +122,7 @@ else() ${OLP_SDK_DATASERVICE_READ_EXAMPLE_TARGET} ${OLP_SDK_DATASERVICE_WRITE_EXAMPLE_TARGET} ${OLP_SDK_DATASERVICE_CACHE_EXAMPLE_TARGET} - ${OLP_SDK_DATASERVICE_READ_STREAM_LAYER_EXAMPLE_TARGET}) + ${OLP_SDK_DATASERVICE_READ_STREAM_LAYER_EXAMPLE_TARGET} + ${OLP_SDK_MTLS_AUTHENTICATION_EXAMPLE_TARGET}) endif() diff --git a/examples/MtlsAuthenticationExample.cpp b/examples/MtlsAuthenticationExample.cpp new file mode 100644 index 000000000..29f8266b9 --- /dev/null +++ b/examples/MtlsAuthenticationExample.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include "MtlsAuthenticationExample.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr auto kLogTag = "mtls-authentication-example"; + +constexpr auto kDiscoverBaseUrl = "https://discover.search.hereapi.com"; +constexpr auto kDiscoverPath = "/v1/discover"; + +// Only the beginning of the response body is logged, the full document can +// be large. +constexpr size_t kMaxLoggedResponseSize = 512; + +std::string ReadFile(const std::string& path) { + std::ifstream stream(path, std::ios::in | std::ios::binary); + if (!stream) { + return {}; + } + std::ostringstream contents; + contents << stream.rdbuf(); + return contents.str(); +} + +// Presents the client certificate during the mTLS handshake to retrieve a +// bearer token. Returns an empty string in case of a failure. +std::string RequestAccessToken(const std::string& cert_path, + const std::string& key_path, + const std::string& ca_path, + const std::string& scope) { + const auto client_cert_pem = ReadFile(cert_path); + const auto client_key_pem = ReadFile(key_path); + + if (client_cert_pem.empty() || client_key_pem.empty()) { + OLP_SDK_LOG_ERROR_F(kLogTag, + "Failed to read certificate or key file, cert='%s', " + "key='%s'", + cert_path.c_str(), key_path.c_str()); + return {}; + } + + olp::authentication::MtlsSettings settings; + settings.mtls_properties.client_cert_pem = client_cert_pem; + settings.mtls_properties.client_key_pem = client_key_pem; + if (!ca_path.empty()) { + settings.mtls_properties.ca_cert_pem = ReadFile(ca_path); + } + if (!scope.empty()) { + settings.mtls_properties.scope = scope; + } + + const olp::authentication::MtlsTokenProviderDefault token_provider( + std::move(settings)); + + olp::client::CancellationContext context; + const auto token_response = token_provider(context); + + if (!token_response.IsSuccessful()) { + OLP_SDK_LOG_ERROR_F( + kLogTag, "mTLS sign in - Failure(%d): %s", + static_cast(token_response.GetError().GetErrorCode()), + token_response.GetError().GetMessage().c_str()); + return {}; + } + + OLP_SDK_LOG_INFO_F(kLogTag, "mTLS sign in - Success, expires in %lld s", + static_cast( + token_response.GetResult().GetExpiresIn().count())); + + return token_response.GetResult().GetAccessToken(); +} + +// Calls the HERE Discover Search API with the access token passed as the +// bearer token. +bool CallDiscoverApi(const std::string& access_token) { + std::shared_ptr http_client = olp::client:: + OlpClientSettingsFactory::CreateDefaultNetworkRequestHandler(); + + const std::multimap query_params = { + {"q", "döner"}, + {"at", "52.53083376480065,13.38469608732926"}, + {"limit", "3"}}; + + const auto url = + olp::utils::Url::Construct(kDiscoverBaseUrl, kDiscoverPath, query_params); + + auto request = olp::http::NetworkRequest(url) + .WithVerb(olp::http::NetworkRequest::HttpVerb::GET) + .WithHeader("Authorization", "Bearer " + access_token); + + auto payload = std::make_shared(); + + std::promise promise; + auto future = promise.get_future(); + + const auto outcome = + http_client->Send(std::move(request), payload, + [&promise](olp::http::NetworkResponse response) { + promise.set_value(std::move(response)); + }); + + if (!outcome.IsSuccessful()) { + OLP_SDK_LOG_ERROR_F( + kLogTag, "Discover request was not sent - Failure: %s", + olp::http::ErrorCodeToString(outcome.GetErrorCode()).c_str()); + return false; + } + + const auto response = future.get(); + + if (response.GetStatus() != olp::http::HttpStatusCode::OK) { + OLP_SDK_LOG_ERROR_F(kLogTag, "Discover request - Failure(%d): %s", + response.GetStatus(), response.GetError().c_str()); + return false; + } + + auto body = payload->str(); + OLP_SDK_LOG_INFO_F(kLogTag, "Discover request - Success, response: %s%s", + body.substr(0, kMaxLoggedResponseSize).c_str(), + body.size() > kMaxLoggedResponseSize ? "..." : ""); + + return true; +} +} // namespace + +int RunExampleMtlsAuthentication(const std::string& cert_path, + const std::string& key_path, + const std::string& ca_path, + const std::string& scope) { + const auto access_token = + RequestAccessToken(cert_path, key_path, ca_path, scope); + if (access_token.empty()) { + return -1; + } + + return CallDiscoverApi(access_token) ? 0 : -1; +} diff --git a/examples/MtlsAuthenticationExample.h b/examples/MtlsAuthenticationExample.h new file mode 100644 index 000000000..fbdd3ebe7 --- /dev/null +++ b/examples/MtlsAuthenticationExample.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include + +#include "Examples.h" + +/** + * @brief mTLS authentication example. Presents a client X.509 certificate + * during the TLS handshake with the HERE mTLS token endpoint to retrieve a + * bearer token, then calls the HERE Discover Search API with that token. + * @param cert_path Path to the client certificate PEM file. + * @param key_path Path to the client private key PEM file. + * @param ca_path (Optional) Path to a CA certificate PEM file. Empty if not + * used. + * @param scope (Optional) The project HRN scope to request. Empty if not + * used. + * @return 0 if the token was retrieved and the Discover API call succeeded. + */ +EXAMPLES_API +int RunExampleMtlsAuthentication(const std::string& cert_path, + const std::string& key_path, + const std::string& ca_path, + const std::string& scope); diff --git a/examples/Options.h b/examples/Options.h index 772225924..301694d0f 100644 --- a/examples/Options.h +++ b/examples/Options.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,14 +32,35 @@ struct Option { const Option kHelpOption{"-h", "--help", "Print the help message and exit."}; -const Option kExampleOption{"-e", "--example", - "Run example [=read|read_stream|write|cache]."}; +const Option kExampleOption{ + "-e", "--example", + "Run example [=read|read_stream|write|cache|mtls-authentication]."}; const Option kKeyIdOption{"-i", "--key-id", "Here key ID to access OLP."}; const Option kKeySecretOption{"-s", "--key-secret", "Here secret key to access OLP."}; +const Option kMtlsCertOption{ + "--cert", "--mtls-cert", + "Path to the client certificate PEM file (required for the " + "mtls-authentication example)."}; + +const Option kMtlsKeyOption{ + "--key", "--mtls-key", + "Path to the client private key PEM file (required for the " + "mtls-authentication example)."}; + +const Option kMtlsCaOption{ + "--ca", "--mtls-ca", + "Path to a CA certificate PEM file (optional, used for the " + "mtls-authentication example)."}; + +const Option kMtlsScopeOption{ + "--scope", "--mtls-scope", + "Project HRN scope to request (optional, used for the " + "mtls-authentication example)."}; + const Option kCatalogOption{"-c", "--catalog", "Catalog HRN (HERE Resource Name)."}; diff --git a/examples/main.cpp b/examples/main.cpp index 93b3cadd5..8b974fac5 100644 --- a/examples/main.cpp +++ b/examples/main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ * License-Filename: LICENSE */ +#include "MtlsAuthenticationExample.h" #include "Options.h" #include "ProtectedCacheExample.h" #include "ReadExample.h" @@ -37,13 +38,14 @@ enum Examples : int { write_example = 0b10, cache_example = 0b100, read_stream_example = 0b1000, + mtls_authentication_example = 0b10000, all_examples = read_example | write_example | cache_example | read_stream_example }; constexpr auto usage = "usage is \n -a, --all : run all examples \n " - "-e, --example[=read|read_stream|write|cache] \n\tRun " + "-e, --example[=read|read_stream|write|cache|mtls-authentication] \n\tRun " "example\n -i, --key-id \n\there.access.key.id \n -s, --key-secret " "\n\there.access.key.secret \n" " -c, --catalog \n\tCatalog HRN (HERE Resource Name). \n" @@ -59,7 +61,13 @@ constexpr auto usage = "ID and access key secret, see the [Get " "Credentials](https://developer.here.com/olp/documentation/access-control/" "user-guide/topics/get-credentials.html) section in the Terms and " - "Permissions User Guide."; + "Permissions User Guide. \n" + " --cert \n\tPath to the client certificate PEM file (required for " + "mtls-authentication example). \n" + " --key \n\tPath to the client private key PEM file (required for " + "mtls-authentication example). \n" + " --ca \n\tPath to a CA certificate PEM file (optional). \n" + " --scope \n\tProject HRN scope to request (optional)."; int RequiredArgumentError(const tools::Option& arg) { std::cout << "option requires an argument -- '" << arg.short_name << '\'' @@ -71,7 +79,9 @@ int ParseArguments(const int argc, char** argv, AccessKey& access_key, olp::porting::optional& catalog_version, std::string& layer_id, olp::dataservice::read::SubscribeRequest::SubscriptionMode& - subscription_mode) { + subscription_mode, + std::string& mtls_cert_path, std::string& mtls_key_path, + std::string& mtls_ca_path, std::string& mtls_scope) { int examples_to_run = 0; const std::vector arguments(argv + 1, argv + argc); @@ -108,9 +118,12 @@ int ParseArguments(const int argc, char** argv, AccessKey& access_key, examples_to_run = Examples::cache_example; } else if (*it == "read_stream") { examples_to_run = Examples::read_stream_example; + } else if (*it == "mtls-authentication") { + examples_to_run = Examples::mtls_authentication_example; } else { std::cout << "Example was not found. Please use values:read, " - "write, cache, read_stream" + "write, cache, read_stream, authentication, " + "mtls-authentication" << std::endl; return 0; } @@ -152,6 +165,26 @@ int ParseArguments(const int argc, char** argv, AccessKey& access_key, } } else if (IsMatch(*it, tools::kAllOption)) { examples_to_run = Examples::all_examples; + } else if (IsMatch(*it, tools::kMtlsCertOption)) { + if (++it == arguments.end()) { + return RequiredArgumentError(tools::kMtlsCertOption); + } + mtls_cert_path = *it; + } else if (IsMatch(*it, tools::kMtlsKeyOption)) { + if (++it == arguments.end()) { + return RequiredArgumentError(tools::kMtlsKeyOption); + } + mtls_key_path = *it; + } else if (IsMatch(*it, tools::kMtlsCaOption)) { + if (++it == arguments.end()) { + return RequiredArgumentError(tools::kMtlsCaOption); + } + mtls_ca_path = *it; + } else if (IsMatch(*it, tools::kMtlsScopeOption)) { + if (++it == arguments.end()) { + return RequiredArgumentError(tools::kMtlsScopeOption); + } + mtls_scope = *it; } else { fprintf(stderr, usage); } @@ -171,7 +204,11 @@ int RunExamples(const AccessKey& access_key, int examples_to_run, const olp::porting::optional& catalog_version, const std::string& layer_id, olp::dataservice::read::SubscribeRequest::SubscriptionMode - subscription_mode) { + subscription_mode, + const std::string& mtls_cert_path, + const std::string& mtls_key_path, + const std::string& mtls_ca_path, + const std::string& mtls_scope) { if (examples_to_run & Examples::read_example) { std::cout << "Read Example" << std::endl; if (RunExampleRead(access_key, catalog, catalog_version)) { @@ -204,6 +241,15 @@ int RunExamples(const AccessKey& access_key, int examples_to_run, return -1; } } + + if (examples_to_run & Examples::mtls_authentication_example) { + std::cout << "mTLS authentication example" << std::endl; + if (RunExampleMtlsAuthentication(mtls_cert_path, mtls_key_path, + mtls_ca_path, mtls_scope)) { + std::cout << "mTLS Authentication Example failed" << std::endl; + return -1; + } + } return 0; } @@ -217,9 +263,14 @@ int main(int argc, char** argv) { auto subscription_mode = olp::dataservice::read::SubscribeRequest::SubscriptionMode:: kSerial; // subscription mode for read stream layer example + std::string mtls_cert_path; + std::string mtls_key_path; + std::string mtls_ca_path; + std::string mtls_scope; int examples_to_run = ParseArguments(argc, argv, access_key, catalog, catalog_version, layer_id, - subscription_mode); + subscription_mode, mtls_cert_path, mtls_key_path, + mtls_ca_path, mtls_scope); if (examples_to_run == 0) { return 0; } @@ -230,9 +281,12 @@ int main(int argc, char** argv) { << std::endl; } - if (catalog.empty()) { - std::cout << "Please specify catalog. For more information use -h [--help]" + if ((examples_to_run & Examples::mtls_authentication_example) && + (mtls_cert_path.empty() || mtls_key_path.empty())) { + std::cout << "Please specify --cert and --key for the mtls-authentication " + "example. For more information use -h [--help]" << std::endl; + return -1; } if (((examples_to_run & Examples::write_example) || @@ -245,5 +299,6 @@ int main(int argc, char** argv) { } return RunExamples(access_key, examples_to_run, catalog, catalog_version, - layer_id, subscription_mode); + layer_id, subscription_mode, mtls_cert_path, mtls_key_path, + mtls_ca_path, mtls_scope); } diff --git a/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h b/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h index 453dbc16e..28034c7a8 100644 --- a/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h +++ b/olp-cpp-sdk-authentication/include/olp/authentication/AuthenticationClient.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2025 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -334,6 +335,22 @@ class AUTHENTICATION_API AuthenticationClient { SignInProperties properties, SignInClientCallback callback); + /** + * @brief Signs in using an mTLS client certificate. + * + * @param properties The `MtlsProperties` structure. + * @param callback The `SignInUserCallback` method that is called when + * the user sign-in request is completed. If successful, the returned HTTP + * status is 200. If a new account is created as a part of the sign-in + * request, and terms must be accepted, the returned HTTP status is 201. + * Otherwise, check the response error. + * + * @return The `CancellationToken` instance that can be used to cancel + * the request. + */ + client::CancellationToken SignInMtls(MtlsProperties properties, + SignInClientCallback callback); + /** * @brief Signs in with the email and password that you used for * registration via the sign-up API and requests your user access token. diff --git a/olp-cpp-sdk-authentication/include/olp/authentication/MtlsProperties.h b/olp-cpp-sdk-authentication/include/olp/authentication/MtlsProperties.h new file mode 100644 index 000000000..81bfefb67 --- /dev/null +++ b/olp-cpp-sdk-authentication/include/olp/authentication/MtlsProperties.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include + +#include +#include + +namespace olp { +namespace authentication { + +/** + * @brief Properties used to sign in via mTLS client certificate. + */ +struct AUTHENTICATION_API MtlsProperties { + /** + * @brief (Required) The client certificate PEM blob presented during the + * TLS handshake. + */ + std::string client_cert_pem; + + /** + * @brief (Required) The client private key PEM blob matching + * `client_cert_pem`. + */ + std::string client_key_pem; + + /** + * @brief (Optional) The CA certificate PEM blob used to verify the token + * endpoint's server certificate, in addition to the system default CAs. + */ + std::string ca_cert_pem; + + /** + * @brief (Optional) The number of seconds left before the access token + * expires. + * + * Ignored if it is zero or greater than the default expiration time + * supported by the mTLS token endpoint. + */ + std::chrono::seconds expires_in{0}; + + /** + * @brief (Optional) The project scope (HRN) to be assigned to the access + * token. + */ + porting::optional scope{porting::none}; +}; + +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/include/olp/authentication/MtlsSettings.h b/olp-cpp-sdk-authentication/include/olp/authentication/MtlsSettings.h new file mode 100644 index 000000000..7d6d3ce0d --- /dev/null +++ b/olp-cpp-sdk-authentication/include/olp/authentication/MtlsSettings.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace olp { +namespace thread { +class TaskScheduler; +} + +namespace authentication { + +/// The default mTLS token endpoint URL. +static constexpr auto kHereMtlsAccountProductionUrl = + "https://mtls.account.api.here.com/mtls/token"; + +/** + * @brief Configures the `MtlsTokenProvider` instance. + */ +struct AUTHENTICATION_API MtlsSettings { + /** + * @brief The client certificate, key, scope, and expiration settings used + * for the mTLS token request. + */ + MtlsProperties mtls_properties; + + /** + * @brief (Optional) The configuration settings for the network layer. + */ + porting::optional network_proxy_settings; + + /** + * @brief (Optional) The mTLS token endpoint URL. + */ + std::string token_endpoint_url{kHereMtlsAccountProductionUrl}; + + /** + * @brief A collection of settings that controls how failed requests should be + * treated. + */ + client::RetrySettings retry_settings; +}; + +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/include/olp/authentication/MtlsTokenProvider.h b/olp-cpp-sdk-authentication/include/olp/authentication/MtlsTokenProvider.h new file mode 100644 index 000000000..3b8d01c99 --- /dev/null +++ b/olp-cpp-sdk-authentication/include/olp/authentication/MtlsTokenProvider.h @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace olp { +namespace authentication { + +namespace internal { + +class MtlsTokenProviderPrivate; + +/// An implementation of `MtlsTokenProvider`. +/// @note This is a private implementation class for internal use only, and +/// not bound to any API stability promises. Please do not use directly. +class AUTHENTICATION_API MtlsTokenProviderImpl { + public: + /** + * @brief Creates the `MtlsTokenProviderImpl` instance. + * + * @param settings The `MtlsSettings` object that is used to customize the + * mTLS token request. + * @param minimum_validity Sets the minimum validity period of the token + * in seconds. + */ + MtlsTokenProviderImpl(MtlsSettings settings, + std::chrono::seconds minimum_validity); + + /// @copydoc MtlsTokenProvider::operator()(client::CancellationContext&) + client::OauthTokenResponse operator()( + client::CancellationContext& context) const; + + /// @copydoc MtlsTokenProvider::GetErrorResponse() + ErrorResponse GetErrorResponse() const; + + /// @copydoc MtlsTokenProvider::GetHttpStatusCode() + int GetHttpStatusCode() const; + + /// @copydoc MtlsTokenProvider::IsTokenResponseOK() + bool IsTokenResponseOK() const; + + private: + std::shared_ptr impl_; +}; + +} // namespace internal + +/** + * @brief Provides authentication tokens using mTLS. + * + * @tparam MinimumValidity The minimum token validity time (in seconds). + * To use the default `MinimumValidity` value, use the + * `MtlsTokenProviderDefault` typedef. + * + * @see `MtlsTokenProviderDefault` + */ +template +class MtlsTokenProvider { + public: + /** + * @brief Creates the `MtlsTokenProvider` instance with the `settings` + * parameter. + * + * @param settings The settings that configure the mTLS token request. + */ + explicit MtlsTokenProvider(MtlsSettings settings) + : impl_(std::make_shared( + std::move(settings), std::chrono::seconds(MinimumValidity))) {} + + /// A default copy constructor. + MtlsTokenProvider(const MtlsTokenProvider& other) = default; + + /// A default move constructor. + MtlsTokenProvider(MtlsTokenProvider&& other) noexcept = default; + + /// A default copy assignment operator. + MtlsTokenProvider& operator=(const MtlsTokenProvider& other) = default; + + /// A default move assignment operator. + MtlsTokenProvider& operator=(MtlsTokenProvider&& other) noexcept = default; + + /** + * @brief Casts the `MtlsTokenProvider` instance to the `bool` type. + * + * Returns true if the previous token request was successful. + * + * @returns True if the previous token request was successful; false + * otherwise. + */ + operator bool() const { return impl_->IsTokenResponseOK(); } + + /** + * @brief Returns the access token or an error. + * + * @param context Used to cancel the pending token request. + * + * @returns An `OauthTokenResponse` if the response is successful; an + * `ApiError` otherwise. + */ + client::OauthTokenResponse operator()( + client::CancellationContext& context) const { + return impl_->operator()(context); + } + + /** + * @brief Allows the `olp::client::ApiError` object associated + * with the last request to be accessed if the token request is unsuccessful. + * + * @returns An error if the last token request failed. + */ + ErrorResponse GetErrorResponse() const { return impl_->GetErrorResponse(); } + + /** + * @brief Gets the HTTP status code of the last request. + * + * @returns The HTTP code of the last token request if it was successful. + * Otherwise, returns the HTTP 503 Service Unavailable server error. + */ + int GetHttpStatusCode() const { return impl_->GetHttpStatusCode(); } + + private: + std::shared_ptr impl_; +}; + +/// Provides mTLS authentication tokens using the default minimum token +/// validity. +using MtlsTokenProviderDefault = MtlsTokenProvider; + +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/src/AuthenticationClient.cpp b/olp-cpp-sdk-authentication/src/AuthenticationClient.cpp index fdcc517a7..0006deb23 100644 --- a/olp-cpp-sdk-authentication/src/AuthenticationClient.cpp +++ b/olp-cpp-sdk-authentication/src/AuthenticationClient.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,11 @@ client::CancellationToken AuthenticationClient::SignInClient( std::move(callback)); } +client::CancellationToken AuthenticationClient::SignInMtls( + MtlsProperties properties, SignInClientCallback callback) { + return impl_->SignInMtls(std::move(properties), std::move(callback)); +} + client::CancellationToken AuthenticationClient::SignInHereUser( const AuthenticationCredentials& credentials, const UserProperties& properties, const SignInUserCallback& callback) { diff --git a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp index de349240c..c8983b777 100644 --- a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp +++ b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.cpp @@ -38,8 +38,10 @@ #include "SignInUserResultImpl.h" #include "SignOutResultImpl.h" #include "SignUpResultImpl.h" +#include "olp/core/client/OlpClientSettingsFactory.h" #include "olp/core/http/Network.h" #include "olp/core/http/NetworkConstants.h" +#include "olp/core/http/NetworkInitializationSettings.h" #include "olp/core/http/NetworkResponse.h" #include "olp/core/http/NetworkUtils.h" #include "olp/core/logging/Log.h" @@ -88,6 +90,7 @@ constexpr auto kFacebookGrantType = "facebook"; constexpr auto kArcgisGrantType = "arcgis"; constexpr auto kAppleGrantType = "jwtIssNotHERE"; constexpr auto kRefreshGrantType = "refresh_token"; +constexpr auto kMtlsGrantType = "mtls"; constexpr auto kServiceId = "serviceId"; constexpr auto kActions = "actions"; @@ -395,6 +398,74 @@ client::CancellationToken AuthenticationClientImpl::SignInClient( std::move(callback)); } +client::CancellationToken AuthenticationClientImpl::SignInMtls( + MtlsProperties properties, SignInClientCallback callback) { + auto task = [=](client::CancellationContext context) -> SignInClientResponse { + if (context.IsCancelled()) { + return client::ApiError::Cancelled(); + } + + http::NetworkInitializationSettings network_settings; + network_settings.certificate_settings.client_cert_file_blob = + properties.client_cert_pem; + network_settings.certificate_settings.client_key_file_blob = + properties.client_key_pem; + network_settings.certificate_settings.cert_file_blob = + properties.ca_cert_pem; + + auto network = CreateNetworkRequestHandler(network_settings); + if (!network) { + return client::ApiError::NetworkConnection( + "Cannot sign in while offline"); + } + + auto settings = settings_; + settings.network_request_handler = network; + auto client = CreateOlpClient(settings, {}, false); + + const auto request_body = GenerateMtlsBody(properties); + + SignInClientResponse response; + + const auto& retry_settings = settings_.retry_settings; + + for (auto retry = 0; retry < retry_settings.max_attempts; ++retry) { + if (context.IsCancelled()) { + return client::ApiError::Cancelled(); + } + + auto auth_response = client.CallApi({}, "POST", {}, {}, {}, request_body, + kApplicationJson, context); + + const auto status = auth_response.GetStatus(); + if (status < 0) { + response = GetSignInResponse( + auth_response, context, settings_.token_endpoint_url); + + if (status == static_cast(http::ErrorCode::TIMEOUT_ERROR) && + context.IsCancelled()) { + return response; + } + + } else { + response = ParseAuthResponse(status, auth_response.GetRawResponse()); + } + + if (retry_settings.retry_condition(auth_response)) { + RetryDelay(retry_settings, retry); + continue; + } + + break; + } + + return response; + }; + + return AddTask(settings_.task_scheduler, pending_requests_, std::move(task), + std::move(callback)); +} + TimeResponse AuthenticationClientImpl::ParseTimeResponse( std::stringstream& payload) { boost::json::error_code ec; @@ -1012,6 +1083,25 @@ AuthenticationClientImpl::GenerateAuthorizeBody( return std::make_shared(content.begin(), content.end()); } +client::OlpClient::RequestBodyType AuthenticationClientImpl::GenerateMtlsBody( + const MtlsProperties& properties) { + boost::json::object object; + + object[kGrantType] = kMtlsGrantType; + + auto expires_in = static_cast(properties.expires_in.count()); + if (expires_in > 0) { + object[Constants::EXPIRES_IN] = expires_in; + } + + if (properties.scope) { + object[kScope] = *properties.scope; + } + + auto content = boost::json::serialize(object); + return std::make_shared(content.begin(), content.end()); +} + std::string AuthenticationClientImpl::GenerateUid() const { std::lock_guard lock(token_mutex_); { @@ -1021,6 +1111,13 @@ std::string AuthenticationClientImpl::GenerateUid() const { } } +std::shared_ptr +AuthenticationClientImpl::CreateNetworkRequestHandler( + http::NetworkInitializationSettings settings) const { + return client::OlpClientSettingsFactory::CreateDefaultNetworkRequestHandler( + std::move(settings)); +} + AuthenticationClientImpl::RequestTimer AuthenticationClientImpl::CreateRequestTimer( const client::OlpClient& client, diff --git a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h index ba7f13af4..2dd4743da 100644 --- a/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h +++ b/olp-cpp-sdk-authentication/src/AuthenticationClientImpl.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ #include "olp/authentication/AuthenticationClient.h" #include "olp/authentication/AuthenticationSettings.h" #include "olp/authentication/AuthorizeRequest.h" +#include "olp/authentication/MtlsProperties.h" #include "olp/authentication/Types.h" #include "olp/core/client/ApiError.h" #include "olp/core/client/CancellationToken.h" @@ -85,6 +86,9 @@ class AuthenticationClientImpl { SignInProperties properties, SignInClientCallback callback); + client::CancellationToken SignInMtls(MtlsProperties properties, + SignInClientCallback callback); + client::CancellationToken SignInHereUser( const AuthenticationCredentials& credentials, const UserProperties& properties, const SignInUserCallback& callback); @@ -159,6 +163,8 @@ class AuthenticationClientImpl { const std::string& reacceptance_token); client::OlpClient::RequestBodyType GenerateAuthorizeBody( const AuthorizeRequest& properties); + client::OlpClient::RequestBodyType GenerateMtlsBody( + const MtlsProperties& properties); virtual olp::client::HttpResponse CallAuth( const client::OlpClient& client, const std::string& endpoint, @@ -193,6 +199,9 @@ class AuthenticationClientImpl { RequestTimer CreateRequestTimer(const client::OlpClient& client, client::CancellationContext context) const; + virtual std::shared_ptr CreateNetworkRequestHandler( + http::NetworkInitializationSettings settings) const; + std::shared_ptr client_token_cache_; std::shared_ptr user_token_cache_; AuthenticationSettings settings_; diff --git a/olp-cpp-sdk-authentication/src/MtlsTokenProvider.cpp b/olp-cpp-sdk-authentication/src/MtlsTokenProvider.cpp new file mode 100644 index 000000000..fec375cda --- /dev/null +++ b/olp-cpp-sdk-authentication/src/MtlsTokenProvider.cpp @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include + +#include +#include +#include + +#include +#include +#include + +#include "MtlsTokenProviderPrivate.h" + +namespace olp { +namespace authentication { +namespace internal { + +namespace { + +std::chrono::steady_clock::time_point ComputeRefreshTime( + const SignInClientResponse& current_token, + const std::chrono::seconds& minimum_validity) { + auto now = std::chrono::steady_clock::now(); + + if (!current_token) { + return now; + } + + auto expiry_time_chrono = now + current_token.GetResult().GetExpiresIn(); + return (expiry_time_chrono <= now) ? now + : (expiry_time_chrono - minimum_validity); +} + +} // namespace + +MtlsTokenProviderPrivate::MtlsTokenProviderPrivate( + MtlsSettings settings, std::chrono::seconds minimum_validity) + : minimum_validity_(minimum_validity), + mtls_properties_(settings.mtls_properties), + client_(std::make_shared( + MakeAuthenticationSettings(settings))) {} + +client::OauthTokenResponse MtlsTokenProviderPrivate::operator()( + client::CancellationContext& context) const { + const auto response = GetResponse(context); + return response ? client::OauthTokenResponse( + {response.GetResult().GetAccessToken(), + response.GetResult().GetExpiryTime()}) + : client::OauthTokenResponse(response.GetError()); +} + +ErrorResponse MtlsTokenProviderPrivate::GetErrorResponse() const { + client::CancellationContext context; + const auto response = GetResponse(context); + if (response) { + return ErrorResponse{}; + } + + ErrorResponse error_response; + error_response.message = response.GetError().GetMessage(); + return error_response; +} + +int MtlsTokenProviderPrivate::GetHttpStatusCode() const { + client::CancellationContext context; + const auto response = GetResponse(context); + return response ? http::HttpStatusCode::OK + : response.GetError().GetHttpStatusCode(); +} + +bool MtlsTokenProviderPrivate::IsTokenResponseOK() const { + client::CancellationContext context; + return GetResponse(context).IsSuccessful(); +} + +AuthenticationSettings MtlsTokenProviderPrivate::MakeAuthenticationSettings( + const MtlsSettings& settings) { + AuthenticationSettings auth_settings; + auth_settings.token_endpoint_url = settings.token_endpoint_url; + auth_settings.retry_settings = settings.retry_settings; + auth_settings.network_proxy_settings = settings.network_proxy_settings; + return auth_settings; +} + +bool MtlsTokenProviderPrivate::ShouldRefreshNow() const { + return minimum_validity_ <= std::chrono::seconds(0) || + std::chrono::steady_clock::now() >= token_refresh_time_; +} + +SignInClientResponse MtlsTokenProviderPrivate::GetResponse( + client::CancellationContext& context) const { + std::lock_guard lock(request_mutex_); + + if (!ShouldRefreshNow()) { + return current_token_; + } + + if (context.IsCancelled()) { + return SignInClientResponse(client::ApiError::Cancelled()); + } + + auto promise = std::make_shared>(); + auto future = promise->get_future(); + auto auth_client = client_; + auto properties = mtls_properties_; + + if (!context.ExecuteOrCancelled([&, auth_client]() { + return auth_client->SignInMtls( + properties, [promise](SignInClientResponse response) { + promise->set_value(std::move(response)); + }); + })) { + return SignInClientResponse(client::ApiError::Cancelled()); + } + + auto sign_in_response = future.get(); + if (context.IsCancelled()) { + return SignInClientResponse(client::ApiError::Cancelled()); + } + + // `SignInMtls` reports any HTTP status >= 0 (including e.g. 401/403 for a + // rejected client certificate) as a "successful" `Response`, since + // `ParseAuthResponse` always builds a valid `SignInResult` regardless of + // status code. An empty access token is the actual signal that the + // request failed; convert it to a real `client::ApiError` here (mirrors + // `TokenEndpointImpl::RequestToken(CancellationContext&, ...)`), so that + // `current_token_` never caches a bogus "success" and every accessor + // built on top of it (`operator()`, `operator bool`, `GetErrorResponse`, + // `GetHttpStatusCode`) sees the failure. + if (sign_in_response && + sign_in_response.GetResult().GetAccessToken().empty()) { + const auto& sign_in_result = sign_in_response.GetResult(); + auto message = sign_in_result.GetFullMessage(); + if (message.empty()) { + message = sign_in_result.GetErrorResponse().message; + } + sign_in_response = + client::ApiError{sign_in_result.GetStatus(), std::move(message)}; + } + + current_token_ = std::move(sign_in_response); + token_refresh_time_ = ComputeRefreshTime(current_token_, minimum_validity_); + + return current_token_; +} + +MtlsTokenProviderImpl::MtlsTokenProviderImpl( + MtlsSettings settings, std::chrono::seconds minimum_validity) + : impl_(std::make_shared(std::move(settings), + minimum_validity)) {} + +client::OauthTokenResponse MtlsTokenProviderImpl::operator()( + client::CancellationContext& context) const { + return impl_->operator()(context); +} + +ErrorResponse MtlsTokenProviderImpl::GetErrorResponse() const { + return impl_->GetErrorResponse(); +} + +int MtlsTokenProviderImpl::GetHttpStatusCode() const { + return impl_->GetHttpStatusCode(); +} + +bool MtlsTokenProviderImpl::IsTokenResponseOK() const { + return impl_->IsTokenResponseOK(); +} + +} // namespace internal +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/src/MtlsTokenProviderPrivate.h b/olp-cpp-sdk-authentication/src/MtlsTokenProviderPrivate.h new file mode 100644 index 000000000..3219a75c9 --- /dev/null +++ b/olp-cpp-sdk-authentication/src/MtlsTokenProviderPrivate.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include "AuthenticationClientImpl.h" + +namespace olp { +namespace authentication { +namespace internal { + +using SignInClientResponse = AuthenticationClient::SignInClientResponse; + +class MtlsTokenProviderPrivate { + public: + MtlsTokenProviderPrivate(MtlsSettings settings, + std::chrono::seconds minimum_validity); + + client::OauthTokenResponse operator()( + client::CancellationContext& context) const; + + ErrorResponse GetErrorResponse() const; + + int GetHttpStatusCode() const; + + bool IsTokenResponseOK() const; + + protected: + static AuthenticationSettings MakeAuthenticationSettings( + const MtlsSettings& settings); + + bool ShouldRefreshNow() const; + + SignInClientResponse GetResponse(client::CancellationContext& context) const; + + std::chrono::seconds minimum_validity_; + MtlsProperties mtls_properties_; + std::shared_ptr client_; + mutable SignInClientResponse current_token_; + mutable std::chrono::steady_clock::time_point token_refresh_time_; + mutable std::mutex request_mutex_; +}; + +} // namespace internal +} // namespace authentication +} // namespace olp diff --git a/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp b/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp index bcec8173f..4454afb5a 100644 --- a/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp +++ b/olp-cpp-sdk-authentication/tests/AuthenticationClientTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 HERE Europe B.V. + * Copyright (C) 2020-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,49 +23,48 @@ #include +#include #include "AuthenticationClientImpl.h" +#include "AuthenticationClientImplTestable.h" #include "AuthenticationClientUtils.h" +#include "AuthenticationMockedResponses.h" #include "mocks/NetworkMock.h" namespace { constexpr auto kTime = "Fri, 29 May 2020 11:07:45 GMT"; constexpr auto kEpochTime = "Thu, 1 Jan 1970 00:00:00 GMT"; constexpr auto kSummerTime = "Tue, 18 Jun 2024 12:25:35 GMT"; + +using std::placeholders::_1; +using std::placeholders::_2; +using std::placeholders::_3; +using std::placeholders::_4; +using std::placeholders::_5; +using std::placeholders::_6; +using std::placeholders::_7; +using testing::_; +using testing::Contains; +using testing::DoAll; +using testing::ElementsAreArray; +using testing::Not; +using testing::Pair; +using testing::Return; +using testing::SaveArg; +using testing::WithArg; + +constexpr auto kBlob1 = "1st string blob"; +constexpr auto kBlob2 = "2nd string blob"; +constexpr auto kBlob3 = "3rd string blob"; +constexpr auto kMtlsTokenEndpointUrl = + "https://mtls.account.api.here.com/mtls/token"; + } // namespace namespace auth = olp::authentication; namespace client = olp::client; -class AuthenticationClientImplTestable : public auth::AuthenticationClientImpl { - public: - explicit AuthenticationClientImplTestable( - auth::AuthenticationSettings settings) - : AuthenticationClientImpl(settings) {} - - MOCK_METHOD(auth::TimeResponse, GetTimeFromServer, - (client::CancellationContext context, - const client::OlpClient& client), - (const, override)); - - MOCK_METHOD(client::HttpResponse, CallAuth, - (const client::OlpClient&, const std::string&, - client::CancellationContext, - const auth::AuthenticationCredentials&, - client::OlpClient::RequestBodyType, std::time_t, - const std::string&), - (override)); - - client::HttpResponse RealCallAuth( - const client::OlpClient& client, const std::string& endpoint, - client::CancellationContext context, - const auth::AuthenticationCredentials& credentials, - client::OlpClient::RequestBodyType body, std::time_t time, - const std::string& content_type) { - return auth::AuthenticationClientImpl::CallAuth( - client, endpoint, std::move(context), credentials, std::move(body), - time, content_type); - } -}; +using AuthenticationClientImplTestable = + mocks::AuthenticationClientImplTestable; ACTION_P(Wait, time) { std::this_thread::sleep_for(time); } @@ -277,24 +276,6 @@ TEST(AuthenticationClientTest, GenerateAuthorizationHeader) { } TEST(AuthenticationClientTest, SignInWithCustomUrlAndBody) { - // Making CPPLINT happy - using testing::_; - using testing::Contains; - using testing::DoAll; - using testing::ElementsAreArray; - using testing::Not; - using testing::Pair; - using testing::Return; - using testing::SaveArg; - - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - using std::placeholders::_7; - constexpr auto custom_url = "https://example.com/user/login"; const auto custom_body = std::string("custom_body"); olp::http::NetworkRequest expected_request{""}; @@ -331,3 +312,183 @@ TEST(AuthenticationClientTest, SignInWithCustomUrlAndBody) { EXPECT_THAT(expected_request.GetHeaders(), Not(Contains(Pair("Content-Type", _)))); } + +TEST(AuthenticationClientTest, SignInMtls) { + auth::AuthenticationSettings settings; + settings.token_endpoint_url = kMtlsTokenEndpointUrl; + settings.network_request_handler = + std::make_shared>(); + + { + SCOPED_TRACE("Failed to create network"); + + AuthenticationClientImplTestable auth_impl(settings); + + olp::http::NetworkInitializationSettings actual_network_settings; + EXPECT_CALL(auth_impl, CreateNetworkRequestHandler(_)) + .WillOnce(DoAll(SaveArg<0>(&actual_network_settings), Return(nullptr))); + + auth::MtlsProperties properties; + properties.ca_cert_pem = kBlob1; + properties.client_cert_pem = kBlob2; + properties.client_key_pem = kBlob3; + + std::promise + response_promise; + auth_impl.SignInMtls( + properties, + [&](const auth::AuthenticationClient::SignInClientResponse& response) { + response_promise.set_value(response); + }); + + auto request_future = response_promise.get_future(); + auto response = request_future.get(); + EXPECT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), + client::ErrorCode::NetworkConnection); + + const auto& certificates = actual_network_settings.certificate_settings; + EXPECT_EQ(properties.ca_cert_pem, certificates.cert_file_blob); + EXPECT_EQ(properties.client_cert_pem, certificates.client_cert_file_blob); + EXPECT_EQ(properties.client_key_pem, certificates.client_key_file_blob); + } + + { + SCOPED_TRACE("Failed to Send. Retriable error then non retriable"); + + AuthenticationClientImplTestable auth_impl(settings); + + auto auth_network_mock = + std::make_shared>(); + EXPECT_CALL(auth_impl, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock)); + + std::vector actual_requests; + const std::string kScope = "random_scope"; + + EXPECT_CALL(*auth_network_mock, Send) + .WillOnce(DoAll( + WithArg<0>([&](olp::http::NetworkRequest request) { + EXPECT_THAT(request, IsPostRequest(settings.token_endpoint_url)); + EXPECT_THAT(request, + BodyContains("\"scope\":\"" + kScope + "\"")); + actual_requests.emplace_back(std::move(request)); + }), + Return(olp::http::SendOutcome( + olp::http::ErrorCode::NETWORK_OVERLOAD_ERROR)))) + .WillOnce(DoAll( + WithArg<0>([&](olp::http::NetworkRequest request) { + EXPECT_THAT(request, IsPostRequest(settings.token_endpoint_url)); + EXPECT_THAT(request, + BodyContains("\"scope\":\"" + kScope + "\"")); + actual_requests.emplace_back(request); + }), + Return( + olp::http::SendOutcome(olp::http::ErrorCode::UNKNOWN_ERROR)))); + + auth::MtlsProperties properties; + properties.scope = kScope; + + std::promise + response_promise; + auth_impl.SignInMtls( + properties, + [&](const auth::AuthenticationClient::SignInClientResponse& response) { + response_promise.set_value(response); + }); + + auto request_future = response_promise.get_future(); + auto response = request_future.get(); + EXPECT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), client::ErrorCode::Unknown); + + EXPECT_EQ(actual_requests.size(), 2U); + } + + { + SCOPED_TRACE("Failed to Send. Failed all retries"); + + AuthenticationClientImplTestable auth_impl(settings); + + auto auth_network_mock = + std::make_shared>(); + EXPECT_CALL(auth_impl, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock)); + + std::vector actual_requests; + const std::string kScope = "scope 02"; + + EXPECT_CALL(*auth_network_mock, Send) + .Times(settings.retry_settings.max_attempts) + .WillRepeatedly(DoAll( + WithArg<0>([&](olp::http::NetworkRequest request) { + EXPECT_THAT(request, IsPostRequest(settings.token_endpoint_url)); + EXPECT_THAT(request, + BodyContains("\"scope\":\"" + kScope + "\"")); + actual_requests.emplace_back(std::move(request)); + }), + Return(olp::http::SendOutcome(olp::http::ErrorCode::IO_ERROR)))); + + auth::MtlsProperties properties; + properties.scope = kScope; + + std::promise + response_promise; + auth_impl.SignInMtls( + properties, + [&](const auth::AuthenticationClient::SignInClientResponse& response) { + response_promise.set_value(response); + }); + + auto request_future = response_promise.get_future(); + auto response = request_future.get(); + EXPECT_FALSE(response.IsSuccessful()); + EXPECT_EQ(response.GetError().GetErrorCode(), + client::ErrorCode::NetworkConnection); + + EXPECT_EQ(actual_requests.size(), settings.retry_settings.max_attempts); + } + + { + SCOPED_TRACE("Success"); + + AuthenticationClientImplTestable auth_impl(settings); + + auto auth_network_mock = + std::make_shared>(); + EXPECT_CALL(auth_impl, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock)); + + const std::string kScope = "scope"; + + EXPECT_CALL(*auth_network_mock, Send) + .WillOnce(DoAll( + WithArg<0>([&](olp::http::NetworkRequest request) { + EXPECT_THAT(request, IsPostRequest(settings.token_endpoint_url)); + EXPECT_THAT(request, + BodyContains("\"scope\":\"" + kScope + "\"")); + }), + ReturnHttpResponse(GetResponse(olp::http::HttpStatusCode::OK), + kResponseWithScope))); + + auth::MtlsProperties properties; + properties.scope = kScope; + + std::promise + response_promise; + auth_impl.SignInMtls( + properties, + [&](const auth::AuthenticationClient::SignInClientResponse& response) { + response_promise.set_value(response); + }); + + auto request_future = response_promise.get_future(); + auto response = request_future.get(); + + EXPECT_TRUE(response.IsSuccessful()); + EXPECT_FALSE(response.GetResult().GetAccessToken().empty()); + EXPECT_EQ(kResponseToken, response.GetResult().GetAccessToken()); + EXPECT_EQ("bearer", response.GetResult().GetTokenType()); + EXPECT_EQ(response.GetResult().GetScope(), kScope); + } +} diff --git a/tests/common/AuthenticationClientImplTestable.h b/tests/common/AuthenticationClientImplTestable.h new file mode 100644 index 000000000..e997cff1a --- /dev/null +++ b/tests/common/AuthenticationClientImplTestable.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2026 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include "AuthenticationClientImpl.h" + +namespace mocks { + +class AuthenticationClientImplTestable + : public olp::authentication::AuthenticationClientImpl { + public: + explicit AuthenticationClientImplTestable( + olp::authentication::AuthenticationSettings settings) + : AuthenticationClientImpl(settings) {} + + MOCK_METHOD(olp::authentication::TimeResponse, GetTimeFromServer, + (olp::client::CancellationContext context, + const olp::client::OlpClient& client), + (const, override)); + + MOCK_METHOD(olp::client::HttpResponse, CallAuth, + (const olp::client::OlpClient&, const std::string&, + olp::client::CancellationContext, + const olp::authentication::AuthenticationCredentials&, + olp::client::OlpClient::RequestBodyType, std::time_t, + const std::string&), + (override)); + + MOCK_METHOD(std::shared_ptr, CreateNetworkRequestHandler, + (olp::http::NetworkInitializationSettings settings), + (const, override)); + + olp::client::HttpResponse RealCallAuth( + const olp::client::OlpClient& client, const std::string& endpoint, + olp::client::CancellationContext context, + const olp::authentication::AuthenticationCredentials& credentials, + olp::client::OlpClient::RequestBodyType body, std::time_t time, + const std::string& content_type) { + return olp::authentication::AuthenticationClientImpl::CallAuth( + client, endpoint, std::move(context), credentials, std::move(body), + time, content_type); + } +}; + +} // namespace mocks diff --git a/tests/integration/olp-cpp-sdk-authentication/AuthenticationMockedResponses.h b/tests/common/AuthenticationMockedResponses.h similarity index 99% rename from tests/integration/olp-cpp-sdk-authentication/AuthenticationMockedResponses.h rename to tests/common/AuthenticationMockedResponses.h index 1cde2abfc..557f5be0e 100644 --- a/tests/integration/olp-cpp-sdk-authentication/AuthenticationMockedResponses.h +++ b/tests/common/AuthenticationMockedResponses.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/tests/common/CMakeLists.txt b/tests/common/CMakeLists.txt index c3331f672..9fdf19d8b 100644 --- a/tests/common/CMakeLists.txt +++ b/tests/common/CMakeLists.txt @@ -21,6 +21,8 @@ set(OLP_SDK_TESTS_COMMON_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/mocks/NetworkMock.h ${CMAKE_CURRENT_SOURCE_DIR}/mocks/TaskSchedulerMock.h ${CMAKE_CURRENT_SOURCE_DIR}/ApiDefaultResponses.h + ${CMAKE_CURRENT_SOURCE_DIR}/AuthenticationClientImplTestable.h + ${CMAKE_CURRENT_SOURCE_DIR}/AuthenticationMockedResponses.h ${CMAKE_CURRENT_SOURCE_DIR}/KeyValueCacheTestable.h ${CMAKE_CURRENT_SOURCE_DIR}/PlatformUrlsGenerator.h ${CMAKE_CURRENT_SOURCE_DIR}/ReadDefaultResponses.h diff --git a/tests/integration/olp-cpp-sdk-authentication/TokenProviderTest.cpp b/tests/integration/olp-cpp-sdk-authentication/TokenProviderTest.cpp index 9e5736152..f527bf0c4 100644 --- a/tests/integration/olp-cpp-sdk-authentication/TokenProviderTest.cpp +++ b/tests/integration/olp-cpp-sdk-authentication/TokenProviderTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 HERE Europe B.V. + * Copyright (C) 2019-2026 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,13 +22,17 @@ #include #include #include +#include +#include #include #include #include #include #include #include +#include "AuthenticationClientImplTestable.h" #include "AuthenticationMockedResponses.h" +#include "MtlsTokenProviderPrivate.h" namespace http = olp::http; namespace client = olp::client; @@ -50,6 +54,11 @@ constexpr auto kWaitTimeout = std::chrono::seconds(3); constexpr auto kMaxRetryAttempts = 5; constexpr auto kMinTimeout = 1; constexpr auto kRequestId = 42; +constexpr auto kBlob1 = "blob n1"; +constexpr auto kBlob2 = "blob n2"; +constexpr auto kBlob3 = "blob n3"; +constexpr auto kMtlsTokenEndpointUrl = + "https://mtls.account.api.here.com/mtls/token"; // Request defines static const std::string kTimestampUrl = @@ -550,4 +559,450 @@ TEST_F(TokenProviderTest, CustomEndpoint) { } } +using authentication::internal::MtlsTokenProviderPrivate; + +class MtlsTokenProviderPrivateTestable : public MtlsTokenProviderPrivate { + public: + explicit MtlsTokenProviderPrivateTestable( + authentication::MtlsSettings settings, + std::chrono::seconds minimum_validity) + : MtlsTokenProviderPrivate(std::move(settings), minimum_validity) {} + + void MockSetAuthClient( + std::shared_ptr client) { + client_ = client; + } + + const authentication::MtlsProperties& MockGetMtlsProperties() const { + return mtls_properties_; + } + + std::shared_ptr MockGetClient() { + return client_; + } + + std::chrono::seconds MockGetMinimumValidity() { return minimum_validity_; } +}; + +class MtlsTokenProviderTest : public ::testing::Test { + public: + MtlsTokenProviderTest() = default; + + void SetUp() override { + network_mock_ = std::make_shared>(); + auth_network_mock_ = std::make_shared>(); + + auth_settings_.token_endpoint_url = kMtlsTokenEndpointUrl; + auth_settings_.network_request_handler = network_mock_; + auth_settings_.task_scheduler = + client::OlpClientSettingsFactory::CreateDefaultTaskScheduler(1); + + auth_client_ = std::make_shared( + auth_settings_); + + mtls_settings_.retry_settings.timeout = 10; + mtls_settings_.mtls_properties.ca_cert_pem = kBlob1; + mtls_settings_.mtls_properties.client_cert_pem = kBlob2; + mtls_settings_.mtls_properties.client_key_pem = kBlob3; + } + + std::shared_ptr> network_mock_; + std::shared_ptr> auth_network_mock_; + + std::shared_ptr auth_client_; + authentication::AuthenticationSettings auth_settings_; + + authentication::MtlsSettings mtls_settings_; +}; + +MATCHER_P(MtlsPropertiesEq, expected, "") { + return arg.client_cert_pem == expected.client_cert_pem && + arg.client_key_pem == expected.client_key_pem && + arg.ca_cert_pem == expected.ca_cert_pem && + arg.expires_in == expected.expires_in && arg.scope == expected.scope; +} + +TEST_F(MtlsTokenProviderTest, Creation) { + { + SCOPED_TRACE("MtlsTokenProviderPrivateTestable"); + + const auto minimum_validity = + std::chrono::seconds(authentication::kDefaultMinimumValidity + 1U); + + MtlsTokenProviderPrivateTestable token_provider{mtls_settings_, + minimum_validity}; + + EXPECT_THAT(mtls_settings_.mtls_properties, + MtlsPropertiesEq(token_provider.MockGetMtlsProperties())); + EXPECT_TRUE(token_provider.MockGetClient()); + EXPECT_EQ(minimum_validity, token_provider.MockGetMinimumValidity()); + } + + { + SCOPED_TRACE("MtlsTokenProvider"); + + authentication::MtlsTokenProviderDefault token_provider{mtls_settings_}; + + EXPECT_EQ(token_provider.GetHttpStatusCode(), + static_cast(olp::http::ErrorCode::AUTHORIZATION_ERROR)); + EXPECT_FALSE(token_provider); + } +} + +TEST_F(MtlsTokenProviderTest, SingleTokenMultipleUsers) { + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + { + SCOPED_TRACE("Request token first time"); + + EXPECT_CALL(*auth_network_mock_, Send) + .WillOnce(ReturnHttpResponse(GetResponse(http::HttpStatusCode::OK), + kResponseValidJson)); + + client::CancellationContext context; + const auto token_response = token_provider(context); + + ASSERT_TRUE(token_response); + ASSERT_EQ(token_response.GetResult().GetAccessToken(), kResponseToken); + } + + { + SCOPED_TRACE("Cached token returned until expired"); + + constexpr size_t kCount = 3u; + + for (size_t index = 0; index < kCount; ++index) { + client::CancellationContext context; + const auto token_response = token_provider(context); + + ASSERT_TRUE(token_response); + EXPECT_EQ(token_response.GetResult().GetAccessToken(), kResponseToken); + } + } +} + +TEST_F(MtlsTokenProviderTest, ConcurrentRequests) { + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_network_mock_, Send) + .WillOnce(ReturnHttpResponse(GetResponse(http::HttpStatusCode::OK), + kResponseValidJson)); + + const auto kRequestCount = 5; + std::vector threads; + std::vector> futures; + + for (auto i = 0; i < kRequestCount; ++i) { + auto promise = std::make_shared>(); + threads.emplace_back([&, promise]() { + client::CancellationContext context; + promise->set_value(token_provider(context)); + }); + futures.emplace_back(promise->get_future()); + } + + for (auto i = 0; i < kRequestCount; ++i) { + if (threads[i].joinable()) { + threads[i].join(); + } + auto token_response = futures[i].get(); + ASSERT_TRUE(token_response); + EXPECT_EQ(token_response.GetResult().GetAccessToken(), kResponseToken); + } +} + +TEST_F(MtlsTokenProviderTest, RetrySettings) { + mtls_settings_.retry_settings.max_attempts = kMaxRetryAttempts; + mtls_settings_.retry_settings.timeout = kMinTimeout * 2; + mtls_settings_.retry_settings.connection_timeout = + std::chrono::seconds(kMinTimeout); + mtls_settings_.retry_settings.transfer_timeout = + std::chrono::seconds(kMinTimeout); + + auth_settings_.retry_settings = mtls_settings_.retry_settings; + auth_client_ = + std::make_shared(auth_settings_); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + const auto retry_predicate = testing::Property( + &http::NetworkRequest::GetSettings, + testing::AllOf( + testing::Property( + &http::NetworkSettings::GetConnectionTimeoutDuration, + std::chrono::seconds(kMinTimeout)), + testing::Property(&http::NetworkSettings::GetTransferTimeoutDuration, + std::chrono::seconds(kMinTimeout)))); + + { + SCOPED_TRACE("Max attempts"); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + EXPECT_CALL(*auth_network_mock_, Send(retry_predicate, _, _, _, _)) + .Times(kMaxRetryAttempts) + .WillRepeatedly(ReturnHttpResponse( + GetResponse(http::HttpStatusCode::TOO_MANY_REQUESTS) + .WithError("Too many requests"), + kResponseTooManyRequests)); + + client::CancellationContext context; + const auto token_response = token_provider(context); + + ASSERT_FALSE(token_response); + EXPECT_EQ(token_response.GetError().GetHttpStatusCode(), + http::HttpStatusCode::TOO_MANY_REQUESTS); + } + + { + SCOPED_TRACE("Timeout"); + + std::future async_finish_future; + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce(testing::WithArg<2>([&](http::Network::Callback callback) { + async_finish_future = std::async(std::launch::async, [=]() { + // Oversleep the timeout period. + std::this_thread::sleep_for(std::chrono::seconds(kMinTimeout * 4)); + + callback(http::NetworkResponse() + .WithStatus(http::HttpStatusCode::OK) + .WithRequestId(kRequestId)); + }); + + return http::SendOutcome(kRequestId); + })); + + EXPECT_CALL(*auth_network_mock_, Cancel(kRequestId)).Times(1); + + client::CancellationContext context; + const auto token_response = token_provider(context); + + ASSERT_EQ(async_finish_future.wait_for(kWaitTimeout), + std::future_status::ready); + ASSERT_FALSE(token_response); + EXPECT_EQ(token_response.GetError().GetHttpStatusCode(), + static_cast(http::ErrorCode::TIMEOUT_ERROR)); + } +} + +TEST_F(MtlsTokenProviderTest, CancellableProvider) { + mtls_settings_.retry_settings.max_attempts = 1; // Disable retries + auth_settings_.retry_settings = mtls_settings_.retry_settings; + auth_client_ = + std::make_shared(auth_settings_); + + { + SCOPED_TRACE("TokenResult contains token"); + + const int status_code = http::HttpStatusCode::OK; + + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce( + ReturnHttpResponse(GetResponse(status_code), kResponseValidJson)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + client::CancellationContext context; + const auto token_response = token_provider(context); + ASSERT_TRUE(token_response); + EXPECT_EQ(token_response.GetResult().GetAccessToken(), kResponseToken); + + EXPECT_TRUE(token_provider.IsTokenResponseOK()); + EXPECT_EQ(token_provider.GetHttpStatusCode(), status_code); + EXPECT_EQ(token_provider.GetErrorResponse().code, 0); + } + + { + SCOPED_TRACE("TokenResult contains error"); + + const int status_code = http::HttpStatusCode::TOO_MANY_REQUESTS; + + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + GetResponse(status_code).WithError("Too many requests"), + kResponseTooManyRequests)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + client::CancellationContext context; + const auto token_response = token_provider(context); + ASSERT_FALSE(token_response); + EXPECT_EQ(token_response.GetError().GetHttpStatusCode(), status_code); + EXPECT_EQ(token_response.GetError().GetMessage(), kResponseTooManyRequests); + } + + { + SCOPED_TRACE("GetErrorResponse tries to refresh token"); + + const int status_code = http::HttpStatusCode::TOO_MANY_REQUESTS; + + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + GetResponse(status_code).WithError("Too many requests"), + kResponseTooManyRequests)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + const auto error_response = token_provider.GetErrorResponse(); + EXPECT_EQ(error_response.message, kResponseTooManyRequests); + } + + { + SCOPED_TRACE("IsTokenResponseOK tries to refresh token"); + + const int status_code = http::HttpStatusCode::TOO_MANY_REQUESTS; + + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + GetResponse(status_code).WithError("Too many requests"), + kResponseTooManyRequests)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + EXPECT_FALSE(token_provider.IsTokenResponseOK()); + } + + { + SCOPED_TRACE("GetHttpStatusCode tries to refresh token"); + + const int status_code = http::HttpStatusCode::TOO_MANY_REQUESTS; + + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce(ReturnHttpResponse( + GetResponse(status_code).WithError("Too many requests"), + kResponseTooManyRequests)); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + EXPECT_EQ(token_provider.GetHttpStatusCode(), status_code); + } + + { + SCOPED_TRACE("Token request cancelled"); + + std::future async_finish_future; + std::promise network_wait_promise; + + client::CancellationContext context; + EXPECT_CALL(*auth_network_mock_, + Send(IsPostRequest(kMtlsTokenEndpointUrl), _, _, _, _)) + .WillOnce(testing::WithArg<2>([&](http::Network::Callback callback) { + async_finish_future = std::async(std::launch::async, [&, callback]() { + std::this_thread::sleep_for(std::chrono::seconds(kMinTimeout)); + context.CancelOperation(); + + EXPECT_EQ(network_wait_promise.get_future().wait_for(kWaitTimeout), + std::future_status::ready); + + callback(http::NetworkResponse() + .WithStatus(http::HttpStatusCode::OK) + .WithRequestId(kRequestId)); + }); + + return http::SendOutcome(kRequestId); + })); + + EXPECT_CALL(*auth_network_mock_, Cancel(kRequestId)).Times(1); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(auth_network_mock_)); + + const auto token_response = token_provider(context); + network_wait_promise.set_value(); + + ASSERT_EQ(async_finish_future.wait_for(kWaitTimeout), + std::future_status::ready); + ASSERT_FALSE(token_response); + EXPECT_EQ(token_response.GetError().GetHttpStatusCode(), + static_cast(http::ErrorCode::CANCELLED_ERROR)); + } + + { + SCOPED_TRACE("TokenResponse is not successful"); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + EXPECT_CALL(*auth_client_, CreateNetworkRequestHandler(_)) + .WillOnce(Return(std::shared_ptr>())); + + client::CancellationContext context; + const auto token_response = token_provider(context); + ASSERT_FALSE(token_response); + EXPECT_EQ(token_response.GetError().GetErrorCode(), + client::ErrorCode::NetworkConnection); + EXPECT_EQ(token_response.GetError().GetMessage(), + "Cannot sign in while offline"); + } + + { + SCOPED_TRACE("Already cancelled context"); + + MtlsTokenProviderPrivateTestable token_provider{ + mtls_settings_, authentication::kDefaultMinimumValiditySeconds}; + token_provider.MockSetAuthClient(auth_client_); + + client::CancellationContext context; + context.CancelOperation(); + + const auto token_response = token_provider(context); + ASSERT_FALSE(token_response); + EXPECT_EQ(token_response.GetError().GetErrorCode(), + client::ErrorCode::Cancelled); + } +} + } // namespace