Add Chained Token Credential (#3466)
This commit is contained in:
parent
4053591a3d
commit
e170e98cb1
@ -4,6 +4,8 @@
|
||||
|
||||
### Features Added
|
||||
|
||||
- Added `ChainedTokenCredential`.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### Bugs Fixed
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
@ -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 .)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
74
sdk/identity/azure-identity/src/chained_token_credential.cpp
Normal file
74
sdk/identity/azure-identity/src/chained_token_credential.cpp
Normal 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.");
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -29,7 +29,7 @@ stages:
|
||||
CtestRegex: azure-identity.
|
||||
LiveTestCtestRegex: azure-identity.
|
||||
LineCoverageTarget: 99
|
||||
BranchCoverageTarget: 63
|
||||
BranchCoverageTarget: 62
|
||||
Artifacts:
|
||||
- Name: azure-identity
|
||||
Path: azure-identity
|
||||
|
||||
Loading…
Reference in New Issue
Block a user