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