forked from Azure/azure-sdk-for-cpp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathazure_pipelines_credential.cpp
More file actions
195 lines (167 loc) · 7.43 KB
/
azure_pipelines_credential.cpp
File metadata and controls
195 lines (167 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#include "azure/identity/azure_pipelines_credential.hpp"
#include "private/client_assertion_credential_impl.hpp"
#include "private/identity_log.hpp"
#include "private/package_version.hpp"
#include "private/tenant_id_resolver.hpp"
#include <azure/core/internal/json/json.hpp>
using Azure::Identity::AzurePipelinesCredential;
using Azure::Identity::AzurePipelinesCredentialOptions;
using Azure::Core::Context;
using Azure::Core::Url;
using Azure::Core::_internal::StringExtensions;
using Azure::Core::Credentials::AccessToken;
using Azure::Core::Credentials::AuthenticationException;
using Azure::Core::Credentials::TokenRequestContext;
using Azure::Core::Http::HttpMethod;
using Azure::Core::Http::HttpStatusCode;
using Azure::Core::Http::RawResponse;
using Azure::Core::Http::Request;
using Azure::Core::Http::_internal::HttpPipeline;
using Azure::Core::Json::_internal::json;
using Azure::Identity::_detail::IdentityLog;
using Azure::Identity::_detail::PackageVersion;
using Azure::Identity::_detail::TenantIdResolver;
AzurePipelinesCredential::AzurePipelinesCredential(
std::string tenantId,
std::string clientId,
std::string serviceConnectionId,
std::string systemAccessToken,
AzurePipelinesCredentialOptions const& options)
: TokenCredential("AzurePipelinesCredential"), m_serviceConnectionId(serviceConnectionId),
m_systemAccessToken(systemAccessToken),
m_httpPipeline(HttpPipeline(options, "identity", PackageVersion::ToString(), {}, {}))
{
m_oidcRequestUrl = _detail::DefaultOptionValues::GetOidcRequestUrl();
if (serviceConnectionId.empty())
{
IdentityLog::Write(
IdentityLog::Level::Warning,
"No service connection ID specified for " + GetCredentialName() + ".");
}
if (systemAccessToken.empty())
{
IdentityLog::Write(
IdentityLog::Level::Warning,
"No system access token specified for " + GetCredentialName() + ".");
}
if (m_oidcRequestUrl.empty())
{
IdentityLog::Write(
IdentityLog::Level::Warning,
"No value for environment variable '" + Azure::Identity::_detail::OidcRequestUrlEnvVarName
+ "' needed by " + GetCredentialName() + ". This should be set by Azure Pipelines.");
}
if (TenantIdResolver::IsValidTenantId(tenantId) && !clientId.empty()
&& !serviceConnectionId.empty() && !systemAccessToken.empty() && !m_oidcRequestUrl.empty())
{
ClientAssertionCredentialOptions clientAssertionCredentialOptions{};
// Get the options from the base class (including ClientOptions).
static_cast<Core::Credentials::TokenCredentialOptions&>(clientAssertionCredentialOptions)
= options;
clientAssertionCredentialOptions.AuthorityHost = options.AuthorityHost;
clientAssertionCredentialOptions.AdditionallyAllowedTenants
= options.AdditionallyAllowedTenants;
std::function<std::string(Context const&)> callback
= [this](Context const& context) { return GetAssertion(context); };
// ClientAssertionCredential validates the tenant ID, client ID, and assertion callback and logs
// warning messages otherwise.
m_clientAssertionCredentialImpl = std::make_unique<_detail::ClientAssertionCredentialImpl>(
GetCredentialName(), tenantId, clientId, callback, clientAssertionCredentialOptions);
}
else
{
// Rather than throwing an exception in the ctor, following the pattern in existing credentials
// to log the errors, and defer throwing an exception to the first call of GetToken(). This is
// primarily needed for credentials that are part of the DefaultAzureCredential, which this
// credential is not intended for.
IdentityLog::Write(
IdentityLog::Level::Warning,
"Azure Pipelines environment is not set up for the " + GetCredentialName()
+ " credential to work.");
}
}
Request AzurePipelinesCredential::CreateOidcRequestMessage() const
{
const std::string oidcApiVersion = "7.1";
Url requestUrl = Url(
m_oidcRequestUrl + "?api-version=" + Url::Encode(oidcApiVersion)
+ "&serviceConnectionId=" + Url::Encode(m_serviceConnectionId));
Request request = Request(HttpMethod::Post, requestUrl);
request.SetHeader("content-type", "application/json");
request.SetHeader("authorization", "Bearer " + m_systemAccessToken);
return request;
}
std::string AzurePipelinesCredential::GetOidcTokenResponse(
std::unique_ptr<RawResponse> const& response,
std::string responseBody) const
{
auto const statusCode = response->GetStatusCode();
if (statusCode != HttpStatusCode::Ok)
{
// Include the response because its body, if any, probably contains an error message.
// OK responses aren't included with errors because they probably contain secrets.
std::string message = GetCredentialName() + " : "
+ std::to_string(static_cast<std::underlying_type<decltype(statusCode)>::type>(statusCode))
+ " (" + response->GetReasonPhrase()
+ ") response from the OIDC endpoint. Check service connection ID and Pipeline "
"configuration.\n\n"
+ responseBody;
IdentityLog::Write(IdentityLog::Level::Verbose, message);
throw AuthenticationException(message);
}
json parsedJson;
try
{
parsedJson = Azure::Core::Json::_internal::json::parse(responseBody);
}
catch (json::exception const&)
{
std::string message = GetCredentialName() + " : Cannot parse the response string as JSON.";
IdentityLog::Write(IdentityLog::Level::Verbose, message);
throw AuthenticationException(message);
}
const std::string oidcTokenPropertyName = "oidcToken";
if (!parsedJson.contains(oidcTokenPropertyName) || !parsedJson[oidcTokenPropertyName].is_string())
{
std::string message = GetCredentialName()
+ " : OIDC token not found in response. \nSee Azure::Core::Diagnostics::Logger for details "
"(https://aka.ms/azsdk/cpp/identity/troubleshooting).";
IdentityLog::Write(IdentityLog::Level::Verbose, message);
throw AuthenticationException(message);
}
return parsedJson[oidcTokenPropertyName].get<std::string>();
}
AzurePipelinesCredential::~AzurePipelinesCredential() = default;
std::string AzurePipelinesCredential::GetAssertion(Context const& context) const
{
Azure::Core::Http::Request oidcRequest = CreateOidcRequestMessage();
std::unique_ptr<RawResponse> response = m_httpPipeline.Send(oidcRequest, context);
if (!response)
{
throw AuthenticationException(
GetCredentialName() + " couldn't send OIDC token request: null response.");
}
auto const bodyStream = response->ExtractBodyStream();
auto const bodyVec = bodyStream ? bodyStream->ReadToEnd(context) : response->GetBody();
auto const responseBody
= std::string(reinterpret_cast<char const*>(bodyVec.data()), bodyVec.size());
return GetOidcTokenResponse(response, responseBody);
}
AccessToken AzurePipelinesCredential::GetToken(
TokenRequestContext const& tokenRequestContext,
Context const& context) const
{
if (!m_clientAssertionCredentialImpl)
{
auto const AuthUnavailable = GetCredentialName() + " authentication unavailable. ";
IdentityLog::Write(
IdentityLog::Level::Warning,
AuthUnavailable + "See earlier " + GetCredentialName() + " log messages for details.");
throw AuthenticationException(
AuthUnavailable + "Azure Pipelines environment is not set up correctly.");
}
return m_clientAssertionCredentialImpl->GetToken(
GetCredentialName(), tokenRequestContext, context);
}