Skip to content

feat(auth): expose expires_at on AccessToken - #7391

Closed
bwplotka wants to merge 1 commit into
googleapis:mainfrom
bwplotka:bwplotka/auth-access-token-expires-at
Closed

bwplotka wants to merge 1 commit into
googleapis:mainfrom
bwplotka:bwplotka/auth-access-token-expires-at

Conversation

@bwplotka

@bwplotka bwplotka commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Internal Token already tracks expires_at: Option<tokio::time::Instant>, and credentials::mds parses expires_in from the Metadata Server JSON response, but From<CacheableResource<Token>> for Result<AccessToken> previously discarded it.

External token managers and watch channels (for example, TokenProviderExtension in open-telemetry/otel-arrow) need to know when a token expires or when the next refresh should be scheduled without maintaining a separate hardcoded timer.

This PR exposes pub expires_at: Option<std::time::Instant> on AccessToken and populates it via tokio::time::Instant::into_std.

Note on semver: AccessToken was introduced in #3642 without #[non_exhaustive] (expires_at was originally on AccessToken in that PR and removed right before merge). If cargo semver-checks requires avoiding a new public struct field on AccessToken in 1.x, I am happy to expose a pub fn expires_at(&self) -> Option<std::time::Instant> getter (or apply rust: no-semver-checks if preferred).

Commits:

  • d1ed02292 feat(auth): expose expires_at on AccessToken

Internal Token already tracks expires_at: Option<tokio::time::Instant>, and credentials::mds parses expires_in from the Metadata Server JSON response, but From<CacheableResource<Token>> for Result<AccessToken> previously discarded it.

External token managers and watch channels (such as OpenTelemetry Collector auth extensions) need to know when a token expires or when the next refresh should be scheduled without maintaining a duplicate timer.

Expose expires_at: Option<std::time::Instant> on AccessToken and populate it via tokio::time::Instant::into_std.

Signed-off-by: Bartek Plotka <[email protected]>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds an expires_at field of type Option<std::time::Instant> to the AccessToken struct, updating its Debug implementation, conversion logic, and corresponding tests. While the implementation is solid, the documentation example for converting Instant across processes contains a potential panic risk if the expiration instant is in the past, and it introduces an external dependency on the time crate. It is recommended to use a safe, standard-library-only approach with std::time::SystemTime to prevent runtime panics.

Comment on lines +256 to +262
/// copy an expiration across processes, consider converting it to a
/// `time::OffsetDateTime` first:
///
/// ```
/// # let expires_at = Some(std::time::Instant::now());
/// expires_at.map(|i| time::OffsetDateTime::now_utc() + (i - std::time::Instant::now()));
/// ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The example code has two issues:

  1. Panic Risk: In Rust, subtracting a larger Instant from a smaller one (e.g., i - Instant::now() when i is in the past) will panic. Since tokens can expire, i can easily be in the past, making this example prone to runtime panics.
  2. External Dependency: The example references time::OffsetDateTime from the external time crate, which may not be a dependency of the user's project. Using std::time::SystemTime from the standard library is more portable and self-contained.

Here is a safe, standard-library-only way to convert Instant to SystemTime without any panic risk:

    /// copy an expiration across processes, consider converting it to a
    /// std::time::SystemTime first:
    ///
    /// ```
    /// # let expires_at = Some(std::time::Instant::now());
    /// let now_instant = std::time::Instant::now();
    /// let now_system = std::time::SystemTime::now();
    /// let expires_at_system = expires_at.map(|i| {
    ///     if i >= now_instant {
    ///         now_system + (i - now_instant)
    ///     } else {
    ///         now_system - (now_instant - i)
    ///     }
    /// });
    /// ```

/// # let expires_at = Some(std::time::Instant::now());
/// expires_at.map(|i| time::OffsetDateTime::now_utc() + (i - std::time::Instant::now()));
/// ```
pub expires_at: Option<std::time::Instant>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you call out in the PR description, it is a breaking change to expose the field in this way. An accessor would be fine...

... but are you sure we need to expose it at all? That is complexity we try to hide from the consumers.

@bwplotka

bwplotka commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

No longer necessary, thanks!

@bwplotka bwplotka closed this Nov 8, 2026
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 this pull request may close these issues.

2 participants