Skip to content

Guarantee minimum remaining token lifetime - #1

Merged
gtbuchanan merged 2 commits into
mainfrom
min-token-lifetime
Jul 23, 2026
Merged

Guarantee minimum remaining token lifetime#1
gtbuchanan merged 2 commits into
mainfrom
min-token-lifetime

Conversation

@gtbuchanan

@gtbuchanan gtbuchanan commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

Guarantee that every token az-devops-token prints has at least MinLifetime (20m) of remaining life, forcing a refresh when the cached token is closer to expiry. Also extracts the acquisition logic, cache, and lifetime policy into internal/token, leaving main.go as CLI wiring.

Why

A consumer that caches the token — notably mise's env_cache — would otherwise pin whatever the tool returned for the cache TTL. MSAL hands back a cached access token whose remaining life is variable (as little as minutes before it refreshes), so a cached copy could be served past its own exp, producing sporadic 401s. With a guaranteed minimum lifetime, a consumer can safely cache the token for any TTL below that bound.

This is the upstream fix for the env_cache concern raised on energyworldnet/build#106 (where the cache was reverted pending this).

How

MSAL Go exposes no force-refresh option and refreshes proactively only when Entra sends refresh_in. When the cached token is inside the window, the tool forces a fresh mint by hiding the cached access token on cache reload (StripAccessTokens), so MSAL's silent path redeems the refresh token instead (apps/internal/base: "redeem a cached refresh token"). The refresh token and all other cache sections are preserved.

Tests / checks

  • internal/token unit tests (StripAccessTokens, NeedsForcedRefresh).
  • go vet, gofmt, go build, and a graceful-degradation smoke run all pass.

Follow-up

After release, build reintroduces a mise env_cache with env_cache_ttl safely below the 20m guarantee.

Emit only access tokens with at least MinLifetime (20m) remaining, so a
consumer that caches the token (e.g. mise's env_cache) can never serve it
past its own exp. MSAL Go exposes no force-refresh option and refreshes
proactively only when Entra sends refresh_in; when the cached token is
inside the window, force a fresh mint by hiding the cached access token
on cache reload, so the silent path redeems the refresh token instead.

Extract the acquisition logic, cache, and lifetime policy into
internal/token with tests beside the package; main.go is now CLI wiring.
clientID and tenantID stay in main so the release `-ldflags -X main.*`
path is unchanged.

README: document the lifetime guarantee and fix a stale "device-code
flow" mention (the tool uses an interactive browser sign-in).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gtbuchanan

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2ce7375e-e11c-4caf-a9b0-0adb90503535

📥 Commits

Reviewing files that changed from the base of the PR and between 000bd60 and 0c32604.

📒 Files selected for processing (1)
  • internal/token/token.go

📝 Walkthrough

Walkthrough

The change moves Azure DevOps token scopes, MSAL cache persistence, and minimum-lifetime refresh logic into internal/token, updates CLI wiring to use it, adds focused tests, and documents browser sign-in and token lifetime guarantees.

Changes

Token refresh flow

Layer / File(s) Summary
Token scope and cache persistence
internal/token/token.go
Defines Azure DevOps scopes, a 20-minute minimum lifetime, atomic file-cache persistence, access-token stripping, and lifetime checks.
Silent acquisition and forced refresh
internal/token/token.go
Retries silent acquisition after removing cached access tokens when the initial token is too close to expiration.
CLI authentication wiring and validation
main.go, internal/token/token_test.go
Injects the shared file cache into MSAL and token acquisition, uses centralized scopes, and tests cache-stripping and lifetime-check behavior.
Usage documentation
README.md
Documents browser sign-in and the printed token’s minimum remaining lifetime and refresh behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant token.Silent
  participant MSAL
  participant token.FileCache
  CLI->>token.Silent: Request Azure DevOps token
  token.Silent->>MSAL: AcquireTokenSilent
  MSAL->>token.FileCache: Load cached credentials
  token.FileCache-->>MSAL: Return cache contents
  MSAL-->>token.Silent: Return token and expiry
  token.Silent->>token.FileCache: Drop access token if lifetime is below 20 minutes
  token.Silent->>MSAL: Retry silent acquisition
  MSAL-->>CLI: Return token meeting minimum lifetime
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: enforcing a minimum remaining token lifetime.
Description check ✅ Passed The description is clearly related to the token lifetime guarantee and the internal token refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/token/token.go`:
- Around line 48-60: Update FileCache.Replace to ignore the os.ReadFile error
only when it indicates os.ErrNotExist; propagate all other cache-read failures,
including permission, I/O, and path errors. Preserve the existing
StripAccessTokens and Unmarshal flow for successfully read data.
- Around line 63-77: The FileCache.Export implementation must serialize cache
updates across processes; atomic rename alone can overwrite newer token state
with stale data. Update Export, or the surrounding reload/export flow, to
acquire a cross-process lock covering the latest cache reload through
temporary-file write and rename, then release it reliably on all error paths;
preserve the existing atomic permissions and rename behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cbb9e938-2866-40d6-87ec-dc2550fea7ec

📥 Commits

Reviewing files that changed from the base of the PR and between a016451 and 000bd60.

📒 Files selected for processing (4)
  • README.md
  • internal/token/token.go
  • internal/token/token_test.go
  • main.go

Comment thread internal/token/token.go
Comment thread internal/token/token.go
FileCache.Replace returned nil on any os.ReadFile error, masking a
permission/IO/path failure as an empty cache (i.e. "not signed in") and
tripping golangci-lint nilerr. Ignore only os.ErrNotExist (the normal
pre-login state) and wrap the rest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gtbuchanan
gtbuchanan merged commit 0e5fe50 into main Jul 23, 2026
2 checks passed
@gtbuchanan
gtbuchanan deleted the min-token-lifetime branch July 23, 2026 21:48
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.

1 participant