Skip to content

Commit 3b231e5

Browse files
authored
Enable proactive renewal of Managed Identity tokens. (#5336)
* Enable proactive renewal of Managed Identity tokens. * Address PR feedback - move helpers to anonymous namespace and renames. * Address local variable rename suggestion.
1 parent f348242 commit 3b231e5

10 files changed

+605
-80
lines changed

sdk/identity/azure-identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features Added
66

7+
- [[#4474]](https://github.com/Azure/azure-sdk-for-cpp/issues/4474) Enable proactive renewal of Managed Identity tokens.
8+
79
### Breaking Changes
810

911
### Bugs Fixed

sdk/identity/azure-identity/src/azure_cli_credential.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ AccessToken AzureCliCredential::GetToken(
191191
"accessToken",
192192
"expiresIn",
193193
std::vector<std::string>{"expires_on", "expiresOn"},
194+
"",
195+
false,
194196
GetLocalTimeToUtcDiffSeconds());
195197
}
196198
catch (json::exception const&)

sdk/identity/azure-identity/src/client_certificate_credential.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ AccessToken ClientCertificateCredential::GetToken(
503503
// call it later. Therefore, any capture made here will outlive the possible time frame when the
504504
// lambda might get called.
505505
return m_tokenCache.GetToken(scopesStr, tenantId, tokenRequestContext.MinimumExpiration, [&]() {
506-
return m_tokenCredentialImpl->GetToken(context, [&]() {
506+
return m_tokenCredentialImpl->GetToken(context, false, [&]() {
507507
auto body = m_requestBody;
508508
if (!scopesStr.empty())
509509
{

sdk/identity/azure-identity/src/client_secret_credential.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ AccessToken ClientSecretCredential::GetToken(
8383
// call it later. Therefore, any capture made here will outlive the possible time frame when the
8484
// lambda might get called.
8585
return m_tokenCache.GetToken(scopesStr, tenantId, tokenRequestContext.MinimumExpiration, [&]() {
86-
return m_tokenCredentialImpl->GetToken(context, [&]() {
86+
return m_tokenCredentialImpl->GetToken(context, false, [&]() {
8787
auto body = m_requestBody;
8888

8989
if (!scopesStr.empty())

sdk/identity/azure-identity/src/managed_identity_source.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Azure::Core::Credentials::AccessToken AppServiceManagedIdentitySource::GetToken(
137137
// call it later. Therefore, any capture made here will outlive the possible time frame when the
138138
// lambda might get called.
139139
return m_tokenCache.GetToken(scopesStr, {}, tokenRequestContext.MinimumExpiration, [&]() {
140-
return TokenCredentialImpl::GetToken(context, [&]() {
140+
return TokenCredentialImpl::GetToken(context, true, [&]() {
141141
auto request = std::make_unique<TokenRequest>(m_request);
142142

143143
if (!scopesStr.empty())
@@ -219,7 +219,7 @@ Azure::Core::Credentials::AccessToken CloudShellManagedIdentitySource::GetToken(
219219
// call it later. Therefore, any capture made here will outlive the possible time frame when the
220220
// lambda might get called.
221221
return m_tokenCache.GetToken(scopesStr, {}, tokenRequestContext.MinimumExpiration, [&]() {
222-
return TokenCredentialImpl::GetToken(context, [&]() {
222+
return TokenCredentialImpl::GetToken(context, true, [&]() {
223223
using Azure::Core::Url;
224224
using Azure::Core::Http::HttpMethod;
225225

@@ -320,6 +320,7 @@ Azure::Core::Credentials::AccessToken AzureArcManagedIdentitySource::GetToken(
320320
return m_tokenCache.GetToken(scopesStr, {}, tokenRequestContext.MinimumExpiration, [&]() {
321321
return TokenCredentialImpl::GetToken(
322322
context,
323+
true,
323324
createRequest,
324325
[&](auto const statusCode, auto const& response) -> std::unique_ptr<TokenRequest> {
325326
using Core::Credentials::AuthenticationException;
@@ -418,7 +419,7 @@ Azure::Core::Credentials::AccessToken ImdsManagedIdentitySource::GetToken(
418419
// call it later. Therefore, any capture made here will outlive the possible time frame when the
419420
// lambda might get called.
420421
return m_tokenCache.GetToken(scopesStr, {}, tokenRequestContext.MinimumExpiration, [&]() {
421-
return TokenCredentialImpl::GetToken(context, [&]() {
422+
return TokenCredentialImpl::GetToken(context, true, [&]() {
422423
auto request = std::make_unique<TokenRequest>(m_request);
423424

424425
if (!scopesStr.empty())

sdk/identity/azure-identity/src/private/token_credential_impl.hpp

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ namespace Azure { namespace Identity { namespace _detail {
7070
* @param expiresOnPropertyNames Names of properties in the JSON object that represent token
7171
* expiration as absolute date-time stamp. Can be empty, in which case no attempt to parse the
7272
* corresponding property will be made. Empty string elements will be ignored.
73+
* @param refreshInPropertyName Name of a property in the JSON object that represents when to
74+
* refresh the token in number of seconds from now.
75+
* @param proactiveRenewal A value to indicate whether to refresh tokens, proactively, with half
76+
* lifetime or not.
7377
* @param utcDiffSeconds Optional. If not 0, it represents the difference between the UTC and a
7478
* desired time zone, in seconds. Then, should an RFC3339 timestamp come without a time zone
7579
* information, a corresponding time zone offset will be applied to such timestamp.
@@ -88,6 +92,8 @@ namespace Azure { namespace Identity { namespace _detail {
8892
std::string const& accessTokenPropertyName,
8993
std::string const& expiresInPropertyName,
9094
std::vector<std::string> const& expiresOnPropertyNames,
95+
std::string const& refreshInPropertyName = "",
96+
bool proactiveRenewal = false,
9197
int utcDiffSeconds = 0);
9298

9399
/**
@@ -101,6 +107,10 @@ namespace Azure { namespace Identity { namespace _detail {
101107
* @param expiresOnPropertyName Name of a property in the JSON object that represents token
102108
* expiration as absolute date-time stamp. Can be empty, in which case no attempt to parse it is
103109
* made.
110+
* @param refreshInPropertyName Name of a property in the JSON object that represents
111+
* when to refresh the token in number of seconds from now.
112+
* @param proactiveRenewal A value to indicate whether to refresh tokens, proactively, with half
113+
* lifetime or not.
104114
*
105115
* @return A successfully parsed access token.
106116
*
@@ -110,13 +120,17 @@ namespace Azure { namespace Identity { namespace _detail {
110120
std::string const& jsonString,
111121
std::string const& accessTokenPropertyName,
112122
std::string const& expiresInPropertyName,
113-
std::string const& expiresOnPropertyName)
123+
std::string const& expiresOnPropertyName,
124+
std::string const& refreshInPropertyName = "",
125+
bool proactiveRenewal = false)
114126
{
115127
return ParseToken(
116128
jsonString,
117129
accessTokenPropertyName,
118130
expiresInPropertyName,
119-
std::vector<std::string>{expiresOnPropertyName});
131+
std::vector<std::string>{expiresOnPropertyName},
132+
refreshInPropertyName,
133+
proactiveRenewal);
120134
}
121135

122136
/**
@@ -169,6 +183,8 @@ namespace Azure { namespace Identity { namespace _detail {
169183
* @brief Gets an authentication token.
170184
*
171185
* @param context A context to control the request lifetime.
186+
* @param proactiveRenewal A value to indicate whether to refresh tokens, proactively, with half
187+
* lifetime or not.
172188
* @param createRequest A function to create a token request.
173189
* @param shouldRetry A function to determine whether a response should be retried with
174190
* another request.
@@ -177,6 +193,7 @@ namespace Azure { namespace Identity { namespace _detail {
177193
*/
178194
Core::Credentials::AccessToken GetToken(
179195
Core::Context const& context,
196+
bool proactiveRenewal,
180197
std::function<std::unique_ptr<TokenRequest>()> const& createRequest,
181198
std::function<std::unique_ptr<TokenRequest>(
182199
Core::Http::HttpStatusCode statusCode,

sdk/identity/azure-identity/src/token_credential_impl.cpp

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ using Azure::Core::Http::HttpStatusCode;
3535
using Azure::Core::Http::RawResponse;
3636
using Azure::Core::Json::_internal::json;
3737

38+
using namespace std::chrono_literals;
39+
3840
TokenCredentialImpl::TokenCredentialImpl(TokenCredentialOptions const& options)
3941
: m_httpPipeline(options, "identity", PackageVersion::ToString(), {}, {})
4042
{
@@ -91,6 +93,7 @@ std::string TokenCredentialImpl::FormatScopes(
9193

9294
AccessToken TokenCredentialImpl::GetToken(
9395
Context const& context,
96+
bool proactiveRenewal,
9497
std::function<std::unique_ptr<TokenCredentialImpl::TokenRequest>()> const& createRequest,
9598
std::function<std::unique_ptr<TokenCredentialImpl::TokenRequest>(
9699
HttpStatusCode statusCode,
@@ -140,7 +143,9 @@ AccessToken TokenCredentialImpl::GetToken(
140143
std::string(responseBodyVector.begin(), responseBodyVector.end()),
141144
"access_token",
142145
"expires_in",
143-
"expires_on");
146+
"expires_on",
147+
"refresh_in",
148+
proactiveRenewal);
144149
}
145150
catch (AuthenticationException const&)
146151
{
@@ -224,13 +229,39 @@ std::string TimeZoneOffsetAsString(int utcDiffSeconds)
224229
return os.str();
225230
}
226231

232+
// Proactive renewal by cutting the refresh time in half if the token expires in more than
233+
// 2 hours.
234+
std::chrono::seconds GetProactiveRenewalSeconds(std::chrono::seconds seconds)
235+
{
236+
if (seconds >= std::chrono::seconds(2h))
237+
{
238+
return seconds / 2;
239+
}
240+
else
241+
{
242+
return seconds;
243+
}
244+
}
245+
246+
DateTime GetProactiveRenewalDateTime(std::int64_t posixTimestamp)
247+
{
248+
const DateTime now = DateTime::clock::now();
249+
250+
const auto renewInSeconds = std::chrono::duration_cast<std::chrono::seconds>(
251+
PosixTimeConverter::PosixTimeToDateTime(posixTimestamp) - now);
252+
253+
return DateTime(now + GetProactiveRenewalSeconds(renewInSeconds));
254+
}
255+
227256
} // namespace
228257

229258
AccessToken TokenCredentialImpl::ParseToken(
230259
std::string const& jsonString,
231260
std::string const& accessTokenPropertyName,
232261
std::string const& expiresInPropertyName,
233262
std::vector<std::string> const& expiresOnPropertyNames,
263+
std::string const& refreshInPropertyName,
264+
bool proactiveRenewal,
234265
int utcDiffSeconds)
235266
{
236267
json parsedJson;
@@ -262,6 +293,35 @@ AccessToken TokenCredentialImpl::ParseToken(
262293
accessToken.Token = parsedJson[accessTokenPropertyName].get<std::string>();
263294
accessToken.ExpiresOn = std::chrono::system_clock::now();
264295

296+
// expiresIn = number of seconds until refresh.
297+
// expiresOn = timestamp of refresh expressed as seconds since epoch.
298+
299+
if (!refreshInPropertyName.empty() && parsedJson.contains(refreshInPropertyName))
300+
{
301+
auto const& refreshIn = parsedJson[refreshInPropertyName];
302+
if (refreshIn.is_number_unsigned())
303+
{
304+
try
305+
{
306+
// 'refresh_in' as number (seconds until refresh)
307+
auto const value = refreshIn.get<std::int64_t>();
308+
if (value <= MaxExpirationInSeconds)
309+
{
310+
static_assert(
311+
MaxExpirationInSeconds <= std::numeric_limits<std::int32_t>::max(),
312+
"Can safely cast to int32");
313+
314+
accessToken.ExpiresOn += std::chrono::seconds(static_cast<std::int32_t>(value));
315+
return accessToken;
316+
}
317+
}
318+
catch (std::exception const&)
319+
{
320+
// refreshIn.get<std::int64_t>() has thrown, we may throw later.
321+
}
322+
}
323+
}
324+
265325
if (parsedJson.contains(expiresInPropertyName))
266326
{
267327
auto const& expiresIn = parsedJson[expiresInPropertyName];
@@ -278,7 +338,9 @@ AccessToken TokenCredentialImpl::ParseToken(
278338
MaxExpirationInSeconds <= std::numeric_limits<std::int32_t>::max(),
279339
"Can safely cast to int32");
280340

281-
accessToken.ExpiresOn += std::chrono::seconds(static_cast<std::int32_t>(value));
341+
auto expiresInSeconds = std::chrono::seconds(static_cast<std::int32_t>(value));
342+
accessToken.ExpiresOn
343+
+= proactiveRenewal ? GetProactiveRenewalSeconds(expiresInSeconds) : expiresInSeconds;
282344
return accessToken;
283345
}
284346
}
@@ -297,8 +359,10 @@ AccessToken TokenCredentialImpl::ParseToken(
297359
MaxExpirationInSeconds <= std::numeric_limits<std::int32_t>::max(),
298360
"Can safely cast to int32");
299361

300-
accessToken.ExpiresOn += std::chrono::seconds(static_cast<std::int32_t>(
362+
auto expiresInSeconds = std::chrono::seconds(static_cast<std::int32_t>(
301363
ParseNumericExpiration(expiresIn.get<std::string>(), MaxExpirationInSeconds)));
364+
accessToken.ExpiresOn
365+
+= proactiveRenewal ? GetProactiveRenewalSeconds(expiresInSeconds) : expiresInSeconds;
302366

303367
return accessToken;
304368
}
@@ -342,7 +406,9 @@ AccessToken TokenCredentialImpl::ParseToken(
342406
auto const value = expiresOn.get<std::int64_t>();
343407
if (value <= MaxPosixTimestamp)
344408
{
345-
accessToken.ExpiresOn = PosixTimeConverter::PosixTimeToDateTime(value);
409+
accessToken.ExpiresOn = proactiveRenewal
410+
? GetProactiveRenewalDateTime(value)
411+
: PosixTimeConverter::PosixTimeToDateTime(value);
346412
return accessToken;
347413
}
348414
}
@@ -359,16 +425,23 @@ AccessToken TokenCredentialImpl::ParseToken(
359425
for (auto const& parse : {
360426
std::function<DateTime(std::string const&)>([&](auto const& s) {
361427
// 'expires_on' as RFC3339 date string (absolute timestamp)
362-
return DateTime::Parse(s + tzOffsetStr, DateTime::DateFormat::Rfc3339);
428+
auto dateTime = DateTime::Parse(s + tzOffsetStr, DateTime::DateFormat::Rfc3339);
429+
return proactiveRenewal ? GetProactiveRenewalDateTime(
430+
PosixTimeConverter::DateTimeToPosixTime(dateTime))
431+
: dateTime;
363432
}),
364-
std::function<DateTime(std::string const&)>([](auto const& s) {
433+
std::function<DateTime(std::string const&)>([&](auto const& s) {
365434
// 'expires_on' as numeric string (posix time representing an absolute timestamp)
366-
return PosixTimeConverter::PosixTimeToDateTime(
367-
ParseNumericExpiration(s, MaxPosixTimestamp));
435+
auto value = ParseNumericExpiration(s, MaxPosixTimestamp);
436+
return proactiveRenewal ? GetProactiveRenewalDateTime(value)
437+
: PosixTimeConverter::PosixTimeToDateTime(value);
368438
}),
369-
std::function<DateTime(std::string const&)>([](auto const& s) {
439+
std::function<DateTime(std::string const&)>([&](auto const& s) {
370440
// 'expires_on' as RFC1123 date string (absolute timestamp)
371-
return DateTime::Parse(s, DateTime::DateFormat::Rfc1123);
441+
auto dateTime = DateTime::Parse(s, DateTime::DateFormat::Rfc1123);
442+
return proactiveRenewal ? GetProactiveRenewalDateTime(
443+
PosixTimeConverter::DateTimeToPosixTime(dateTime))
444+
: dateTime;
372445
}),
373446
})
374447
{

sdk/identity/azure-identity/src/workload_identity_credential.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ AccessToken WorkloadIdentityCredential::GetToken(
129129
// call it later. Therefore, any capture made here will outlive the possible time frame when the
130130
// lambda might get called.
131131
return m_tokenCache.GetToken(scopesStr, tenantId, tokenRequestContext.MinimumExpiration, [&]() {
132-
return m_tokenCredentialImpl->GetToken(context, [&]() {
132+
return m_tokenCredentialImpl->GetToken(context, false, [&]() {
133133
auto body = m_requestBody;
134134
if (!scopesStr.empty())
135135
{

0 commit comments

Comments
 (0)