Add Chained Token Credential (#3466)

This commit is contained in:
Anton Kolesnyk 2022-03-28 17:10:25 -07:00 committed by GitHub
parent 4053591a3d
commit e170e98cb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 363 additions and 7 deletions

View File

@ -4,6 +4,8 @@
### Features Added
- Added `ChainedTokenCredential`.
### Breaking Changes
### Bugs Fixed

View File

@ -46,6 +46,7 @@ endif()
set(
AZURE_IDENTITY_HEADER
inc/azure/identity/chained_token_credential.hpp
inc/azure/identity/client_secret_credential.hpp
inc/azure/identity/dll_import_export.hpp
inc/azure/identity/environment_credential.hpp
@ -58,6 +59,7 @@ set(
src/private/managed_identity_source.hpp
src/private/package_version.hpp
src/private/token_credential_impl.hpp
src/chained_token_credential.cpp
src/client_secret_credential.cpp
src/environment_credential.cpp
src/managed_identity_credential.cpp

View File

@ -81,6 +81,20 @@ The [Managed identity authentication](https://docs.microsoft.com/azure/active-di
* [Azure Cloud Shell](https://docs.microsoft.com/azure/cloud-shell/msi-authorization)
* [Azure Arc](https://docs.microsoft.com/azure/azure-arc/servers/managed-identity-authentication)
## Chained Token Credential
`ChainedTokenCredential` allows users to customize the credentials considered when authenticating.
An example below demonstrates using `ChainedTokenCredential` which will attempt to authenticate using `EnvironmentCredential`, and fall back to authenticate using `ManagedIdentityCredential`.
```cpp
// Authenticate using environment credential if it is available; otherwise use the managed identity credential to authenticate.
auto chainedTokenCredential = std::make_shared<Azure::Identity::ChainedTokenCredential>(
Azure::Identity::ChainedTokenCredential::Sources{
std::make_shared<Azure::Identity::EnvironmentCredential>(),
std::make_shared<Azure::Identity::ManagedIdentityCredential>()});
Azure::Service::Client azureServiceClient("serviceUrl", chainedTokenCredential);
```
## Troubleshooting
Credentials raise exceptions either when they fail to authenticate or cannot execute authentication.
When credentials fail to authenticate, the `AuthenticationException` is thrown and it has the `what()` functions returning the description why authentication failed.

View File

@ -8,6 +8,7 @@
#pragma once
#include "azure/identity/chained_token_credential.hpp"
#include "azure/identity/client_secret_credential.hpp"
#include "azure/identity/dll_import_export.hpp"
#include "azure/identity/environment_credential.hpp"

View File

@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
/**
* @file
* @brief Chained Token Credential.
*/
#pragma once
#include <azure/core/credentials/credentials.hpp>
#include <memory>
#include <vector>
namespace Azure { namespace Identity {
/**
* @brief Chained Token Credential provides a token credential implementation which chains
* multiple Azure::Core::Credentials::TokenCredential implementations to be tried in order until
* one of the GetToken() methods returns an access token.
*
*/
class ChainedTokenCredential final : public Core::Credentials::TokenCredential {
public:
/**
* @brief A container type to store the ordered chain of credentials.
*
*/
using Sources = std::vector<std::shared_ptr<Core::Credentials::TokenCredential>>;
/**
* @brief Constructs a Chained Token Credential.
*
* @param sources The ordered chain of Azure::Core::Credentials::TokenCredential implementations
* to try when calling GetToken().
*/
explicit ChainedTokenCredential(Sources sources);
/**
* @brief Destructs `%ChainedTokenCredential`.
*
*/
~ChainedTokenCredential() override;
/**
* @brief Gets an authentication token.
*
* @param tokenRequestContext A context to get the token in.
* @param context A context to control the request lifetime.
*
* @throw Azure::Core::Credentials::AuthenticationException Authentication error occurred.
*/
Core::Credentials::AccessToken GetToken(
Core::Credentials::TokenRequestContext const& tokenRequestContext,
Core::Context const& context) const override;
private:
Sources m_sources;
};
}} // namespace Azure::Identity

View File

@ -7,6 +7,11 @@ project (azure-identity-samples LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)
add_executable(chained_token_credential_sample chained_token_credential.cpp)
target_link_libraries(chained_token_credential_sample PRIVATE azure-identity)
target_include_directories(chained_token_credential_sample PRIVATE .)
create_per_service_target_build_for_sample(identity chained_token_credential_sample)
add_executable(client_secret_credential_sample client_secret_credential.cpp)
target_link_libraries(client_secret_credential_sample PRIVATE azure-identity)
target_include_directories(client_secret_credential_sample PRIVATE .)

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include <iostream>
#include <azure/identity/chained_token_credential.hpp>
#include <azure/identity/environment_credential.hpp>
#include <azure/identity/managed_identity_credential.hpp>
#include <azure/service/client.hpp>
int main()
{
try
{
// Step 1: Initialize Chained Token Credential.
// A configuration demonstrated below would authenticate using EnvironmentCredential if it is
// available, and if it is not available, would fall back to use ManagedIdentityCredential.
auto chainedTokenCredential = std::make_shared<Azure::Identity::ChainedTokenCredential>(
Azure::Identity::ChainedTokenCredential::Sources{
std::make_shared<Azure::Identity::EnvironmentCredential>(),
std::make_shared<Azure::Identity::ManagedIdentityCredential>()});
// Step 2: Pass the credential to an Azure Service Client.
Azure::Service::Client azureServiceClient("serviceUrl", chainedTokenCredential);
// Step 3: Start using the Azure Service Client.
azureServiceClient.DoSomething(Azure::Core::Context::ApplicationContext);
std::cout << "Success!" << std::endl;
}
catch (const Azure::Core::Credentials::AuthenticationException& exception)
{
// Step 4: Handle authentication errors, if needed
// (invalid credential parameters, insufficient permissions).
std::cout << "Authentication error: " << exception.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -23,7 +23,7 @@ int main()
GetTenantId(), GetClientId(), GetClientSecret());
// Step 2: Pass the credential to an Azure Service Client.
Azure::Service::Client azureServiceClient("some parameter", clientSecretCredential);
Azure::Service::Client azureServiceClient("serviceUrl", clientSecretCredential);
// Step 3: Start using the Azure Service Client.
azureServiceClient.DoSomething(Azure::Core::Context::ApplicationContext);
@ -32,8 +32,11 @@ int main()
}
catch (const Azure::Core::Credentials::AuthenticationException& exception)
{
// Step 4 (optional/oversimplified): Handle authentication errors
// Step 4: Handle authentication errors, if needed
// (invalid credential parameters, insufficient permissions).
std::cout << "Authentication error: " << exception.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -17,7 +17,7 @@ int main()
auto environmentCredential = std::make_shared<Azure::Identity::EnvironmentCredential>();
// Step 2: Pass the credential to an Azure Service Client.
Azure::Service::Client azureServiceClient("some parameter", environmentCredential);
Azure::Service::Client azureServiceClient("serviceUrl", environmentCredential);
// Step 3: Start using the Azure Service Client.
azureServiceClient.DoSomething(Azure::Core::Context::ApplicationContext);
@ -26,8 +26,11 @@ int main()
}
catch (const Azure::Core::Credentials::AuthenticationException& exception)
{
// Step 4 (optional/oversimplified): Handle authentication errors
// Step 4: Handle authentication errors, if needed
// (invalid credential parameters, insufficient permissions).
std::cout << "Authentication error: " << exception.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -17,7 +17,7 @@ int main()
auto managedIdentityCredential = std::make_shared<Azure::Identity::ManagedIdentityCredential>();
// Step 2: Pass the credential to an Azure Service Client.
Azure::Service::Client azureServiceClient("some parameter", managedIdentityCredential);
Azure::Service::Client azureServiceClient("serviceUrl", managedIdentityCredential);
// Step 3: Start using the Azure Service Client.
azureServiceClient.DoSomething(Azure::Core::Context::ApplicationContext);
@ -26,8 +26,11 @@ int main()
}
catch (const Azure::Core::Credentials::AuthenticationException& exception)
{
// Step 4 (optional/oversimplified): Handle authentication errors
// Step 4: Handle authentication errors, if needed
// (invalid credential parameters, insufficient permissions).
std::cout << "Authentication error: " << exception.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include "azure/identity/chained_token_credential.hpp"
#include "azure/core/internal/diagnostics/log.hpp"
#include <utility>
using namespace Azure::Identity;
using namespace Azure::Core::Credentials;
using Azure::Core::Context;
ChainedTokenCredential::ChainedTokenCredential(ChainedTokenCredential::Sources sources)
: m_sources(std::move(sources))
{
}
ChainedTokenCredential::~ChainedTokenCredential() = default;
AccessToken ChainedTokenCredential::GetToken(
TokenRequestContext const& tokenRequestContext,
Context const& context) const
{
using Azure::Core::Diagnostics::Logger;
using Azure::Core::Diagnostics::_internal::Log;
auto n = 0;
for (const auto& source : m_sources)
{
try
{
++n;
auto token = source->GetToken(tokenRequestContext, context);
{
const auto logLevel = Logger::Level::Informational;
if (Log::ShouldWrite(logLevel))
{
Log::Write(
logLevel,
std::string("ChainedTokenCredential authentication attempt with credential #")
+ std::to_string(n) + " did succeed.");
}
}
return token;
}
catch (const AuthenticationException& e)
{
const auto logLevel = Logger::Level::Verbose;
if (Log::ShouldWrite(logLevel))
{
Log::Write(
logLevel,
std::string("ChainedTokenCredential authentication attempt with credential #")
+ std::to_string(n) + " did not succeed: " + e.what());
}
}
}
if (n == 0)
{
const auto logLevel = Logger::Level::Verbose;
if (Log::ShouldWrite(logLevel))
{
Log::Write(
logLevel,
"ChainedTokenCredential authentication did not succeed: list of sources is empty.");
}
}
throw AuthenticationException("Failed to get token from ChainedTokenCredential.");
}

View File

@ -16,6 +16,7 @@ add_compile_definitions(AZURE_TEST_RECORDING_DIR="${CMAKE_CURRENT_LIST_DIR}")
add_executable (
azure-identity-test
chained_token_credential_test.cpp
client_secret_credential_test.cpp
credential_test_helper.cpp
credential_test_helper.hpp

View File

@ -0,0 +1,145 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// SPDX-License-Identifier: MIT
#include "azure/identity/chained_token_credential.hpp"
#include <azure/core/diagnostics/logger.hpp>
#include <string>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
using Azure::Identity::ChainedTokenCredential;
using Azure::Core::Context;
using Azure::Core::Credentials::AccessToken;
using Azure::Core::Credentials::AuthenticationException;
using Azure::Core::Credentials::TokenCredential;
using Azure::Core::Credentials::TokenRequestContext;
namespace {
class TestCredential : public TokenCredential {
private:
std::string m_token;
public:
TestCredential(std::string token = "") : m_token(token) {}
mutable bool WasInvoked = false;
AccessToken GetToken(TokenRequestContext const&, Context const&) const override
{
WasInvoked = true;
if (m_token.empty())
{
throw AuthenticationException("Test Error");
}
AccessToken token;
token.Token = m_token;
return token;
}
};
} // namespace
TEST(ChainedTokenCredential, Success)
{
auto c1 = std::make_shared<TestCredential>("Token1");
auto c2 = std::make_shared<TestCredential>("Token2");
ChainedTokenCredential cred({c1, c2});
EXPECT_FALSE(c1->WasInvoked);
EXPECT_FALSE(c2->WasInvoked);
auto token = cred.GetToken({}, {});
EXPECT_EQ(token.Token, "Token1");
EXPECT_TRUE(c1->WasInvoked);
EXPECT_FALSE(c2->WasInvoked);
}
TEST(ChainedTokenCredential, Empty)
{
ChainedTokenCredential cred({});
EXPECT_THROW(cred.GetToken({}, {}), AuthenticationException);
}
TEST(ChainedTokenCredential, ErrorThenSuccess)
{
auto c1 = std::make_shared<TestCredential>();
auto c2 = std::make_shared<TestCredential>("Token2");
ChainedTokenCredential cred({c1, c2});
EXPECT_FALSE(c1->WasInvoked);
EXPECT_FALSE(c2->WasInvoked);
auto token = cred.GetToken({}, {});
EXPECT_EQ(token.Token, "Token2");
EXPECT_TRUE(c1->WasInvoked);
EXPECT_TRUE(c2->WasInvoked);
}
TEST(ChainedTokenCredential, AllErrors)
{
auto c1 = std::make_shared<TestCredential>();
auto c2 = std::make_shared<TestCredential>();
ChainedTokenCredential cred({c1, c2});
EXPECT_FALSE(c1->WasInvoked);
EXPECT_FALSE(c2->WasInvoked);
EXPECT_THROW(cred.GetToken({}, {}), AuthenticationException);
EXPECT_TRUE(c1->WasInvoked);
EXPECT_TRUE(c2->WasInvoked);
}
TEST(ChainedTokenCredential, Logging)
{
using Azure::Core::Diagnostics::Logger;
using LogMsgVec = std::vector<std::pair<Logger::Level, std::string>>;
LogMsgVec log;
Logger::SetLevel(Logger::Level::Verbose);
Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); });
ChainedTokenCredential c0({});
EXPECT_THROW(c0.GetToken({}, {}), AuthenticationException);
EXPECT_EQ(log.size(), LogMsgVec::size_type(1));
EXPECT_EQ(log[0].first, Logger::Level::Verbose);
EXPECT_EQ(
log[0].second,
"ChainedTokenCredential authentication did not succeed: list of sources is empty.");
log.clear();
auto c1 = std::make_shared<TestCredential>();
auto c2 = std::make_shared<TestCredential>("Token2");
ChainedTokenCredential cred({c1, c2});
EXPECT_FALSE(c1->WasInvoked);
EXPECT_FALSE(c2->WasInvoked);
auto token = cred.GetToken({}, {});
EXPECT_EQ(token.Token, "Token2");
EXPECT_TRUE(c1->WasInvoked);
EXPECT_TRUE(c2->WasInvoked);
EXPECT_EQ(log.size(), LogMsgVec::size_type(2));
EXPECT_EQ(log[0].first, Logger::Level::Verbose);
EXPECT_EQ(
log[0].second,
"ChainedTokenCredential authentication attempt with credential #1 did not succeed: "
"Test Error");
EXPECT_EQ(log[1].first, Logger::Level::Informational);
EXPECT_EQ(
log[1].second,
"ChainedTokenCredential authentication attempt with credential #2 did succeed.");
Logger::SetListener(nullptr);
}

View File

@ -29,7 +29,7 @@ stages:
CtestRegex: azure-identity.
LiveTestCtestRegex: azure-identity.
LineCoverageTarget: 99
BranchCoverageTarget: 63
BranchCoverageTarget: 62
Artifacts:
- Name: azure-identity
Path: azure-identity