Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AzureCliCredential parses an incorrect token expiry time in multi-threaded Unix applications #1371

Closed
pencels opened this issue Sep 13, 2023 · 4 comments

Comments

@pencels
Copy link

pencels commented Sep 13, 2023

Description

To get an access token, AzureCliCredential shells out to az account get-access-token and parses up the expiresOn string as a datetime value. Since az formats the datetime without a timezone offset, it assumes the offset with date::assume_local().

pub fn parse(s: &str) -> azure_core::Result<OffsetDateTime> {
// expiresOn from azure cli uses the local timezone and needs to be converted to UTC
let dt = PrimitiveDateTime::parse(s, FORMAT)
.with_context(ErrorKind::DataConversion, || {
format!("unable to parse expiresOn '{s}")
})?;
Ok(date::assume_local(&dt))
}

date::assume_local() delegates to time's function UtcOffset::current_local_offset().

/// Assumes the local offset. Default to UTC if unable to get local offset.
pub fn assume_local(date: &PrimitiveDateTime) -> OffsetDateTime {
date.assume_offset(UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC))
}

For most Unix systems, UtcOffset::current_local_offset() will return Ok(offset) for single-threaded applications, and Err(IndeterminateOffset) for multi-threaded applications, as defined by this function.

In my case, I am running my application on Ubuntu (WSL), and my application is multi-threaded. So this offset is always indeterminate.

(Aside: There's a whole conversation about thread-safety of accessing local time zone data, which is the reason why this behavior exists. If certain things change in the Rust ecosystem, then this problem may just resolve itself as simply as installing a new release of time.)

Because the offset is indeterminate, date::assume_local() will default the expiry time to being in the UTC time zone, when it should be in my local (PDT) time zone. This is obviously incorrect behavior, because the expiry time is now hours off from the actual time it should represent.

This can manifest as bugs further down the line, e.g. in AutoRefreshingTokenCredential where recently cached tokens are refreshed constantly because they are viewed as expired. Each token refresh adds consistent overhead to SDK calls. In particular, this led my calls through SDK clients to take approximately ~1.3s each rather than an expected ~90ms each when the token was only refreshed as needed.

Workarounds/Solutions

  • As users of the SDK, what we can do to mitigate this:
    • (RECOMMENDED) Use other credential types. This will avoid the issue, simply because other credential types don't interact with this code.
    • (NOT recommended) Manually override the thread-count check by using the set_soundness() function provided by time. This requires some understanding of the thread-safety of your application and probably isn't worth the effort to do correctly.
  • As SDK authors, what we can do to fix this:
    • Wait for the Rust ecosystem to figure out a way to manage thread-safe access of local time information, then consume this new thread-safe interface.
    • Push for changes in the Azure CLI output. It seems az-cli was looking into representing the expiry time as an integer timestamp instead (az account get-access-token: Use epoch expiresOn/expires_on azure-cli#19700). If implemented, the AzureCliCredential would just have to deserialize the epoch timestamp, circumventing the need to determine the system's local offset. However, I haven't seen much activity on this issue, so I'm not sure what the timelines are there.
    • We could also try to use set_soundness() in the SDK itself, but this is probably unsafe. Again, not recommended.

Relevant Issues/PRs

@demoray
Copy link
Contributor

demoray commented Sep 13, 2023

@cataggar, with the recent changes made to chrono, we might want to revisit our migration from chrono to time as it relates to this. Specifically, chrono no longer relies on libc::localtime_r, instead replies on code originally from the tz-rs crate to handle this in a thread-safe fashion.

Ref: https://github.com/chronotope/chrono/releases/tag/v0.4.30

@cataggar
Copy link
Member

cataggar commented Sep 17, 2023

I think it is best that the Azure CLI pass the unix timestamp along.
I opened Azure/azure-cli#27410 to see if we could this going again.

@cataggar
Copy link
Member

My PR to pass the unix timestamp was replaced with Azure/azure-cli#27476

demoray pushed a commit to demoray/azure-sdk-for-rust that referenced this issue Oct 3, 2023
As indicated in Azure#1371, there are numerous issues with handling the
az-cli's output of an access token's expiration date when it comes to
time zones.

An update to the Azure CLI is in progress that will add a _new_ field
that will handle the conversion, currently tracked as Azure/azure-cli#27476.

We should make use of this new feature once it's available, but we
should also keep this functionality as a fallback for supporting older
Azure CLIs.
demoray added a commit that referenced this issue Oct 3, 2023
As indicated in #1371, there are numerous issues with handling the
az-cli's output of an access token's expiration date when it comes to
time zones.

An update to the Azure CLI is in progress that will add a _new_ field
that will handle the conversion, currently tracked as Azure/azure-cli#27476.

We should make use of this new feature once it's available, but we
should also keep this functionality as a fallback for supporting older
Azure CLIs.
@demoray
Copy link
Contributor

demoray commented Oct 4, 2023

This has been mitigated in 0.16.2, released earlier today. In the future, we should make use of the az-cli feature that was linked earlier if the functionality is available.

For existing az-cli instances, we use tz-rs to attempt to identify and resolve the timezone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants