feat: expose external auth token expiry in agent API and CLI - #26883
Merged
Conversation
Surface OAuthExpiry from the database through ExternalAuthResponse so workspace agents and credential helpers can cache tokens until the right moment rather than round-tripping to coderd on every git operation. Adds ExpiresAt to ExternalAuthResponse, threads the expiry through createExternalAuthResponse (normalizing to UTC to avoid sub-minute LMT timezone precision loss in JSON encoding), and exposes it via --output json on coder external-auth access-token.
Docs preview📖 View docs preview for |
BobbyHo
marked this pull request as ready for review
June 30, 2026 21:04
cstyan
reviewed
Jul 6, 2026
…esponse The test calls createExternalAuthResponse directly in-process, so there is no JSON round-trip to protect against, and dbtime.Now() already strips the monotonic clock reading via Round. The truncation only masked whether sub-second precision survives createExternalAuthResponse, which does not truncate.
…ption "Print the full token response as JSON" already implies expiry is included since ExpiresAt is a field on the response.
The json and text output branches both checked extAuth.URL to decide whether to return cliui.ErrCanceled. Move that check after the output switch so it happens once regardless of output format.
JSONOutputWithExpiry and JSONOutputWithURL duplicated identical server/invocation/unmarshal scaffolding and only differed in the response payload and expected exit outcome. Consolidate into a table-driven JSONOutput test with per-case subtests.
…lAuthResponse GitHub, GitLab, BitbucketCloud, and BitbucketServer subtests were near-identical, differing only in provider type, token, and expected Username/Password pair. Consolidate into a single table-driven loop. WithExpiry, ZeroExpiry, WithTokenExtra, and InvalidExtraJSON test distinct behaviors and stay as standalone subtests.
…example ExternalAuthResponse is public SDK surface, so callers outside this repo may rely on the doc comment rather than the implementation.
The Makefile's coderd/apidoc/.gen prerequisites only glob codersdk/*.go, not codersdk/agentsdk/*.go, so the earlier doc comment change in codersdk/agentsdk/agentsdk.go did not trigger a local regeneration and CI's gen check caught the stale swagger output.
Contributor
Author
|
Thank you @cstyan for reviewing this PR and sharing the detailed comments. I’ve updated the code based on your feedback. Would you mind taking another look when you have a moment? Thanks! |
cstyan
approved these changes
Jul 7, 2026
cstyan
left a comment
Contributor
There was a problem hiding this comment.
LGTM, tests look much more concise than before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Previously, `ExternalAuthResponse` contained no expiry information, so workspace agents and git credential helpers had no way to know when a cached token would stop being valid. Every git operation had to call back to coderd via `GIT_ASKPASS` to get a fresh token, adding 1-2 seconds of latency.
This PR surfaces `OAuthExpiry` from the database as `ExpiresAt` in `ExternalAuthResponse`, allowing agents to cache tokens with correct eviction timing (compatible with `git-credential-cache --timeout` and `password_expiry_utc` introduced in git 2.34).
`ExpiresAt` is normalized to UTC before JSON encoding to avoid sub-minute precision loss that occurs when the PostgreSQL driver applies historical Local Mean Time (LMT) timezone offsets to year-1 AD timestamps.
The `coder external-auth access-token` CLI command gains `--output json` to print the full response including `ExpiresAt`, enabling scripts to consume the expiry without parsing heuristics.
Closes #26036
Manual Test
Setup
Create a GitHub OAuth app at https://github.com/settings/developers with:
http://127.0.0.1:3000http://127.0.0.1:3000/external-auth/github/callbackStart the dev server with the GitHub provider configured:
Log in at
http://127.0.0.1:3000(use127.0.0.1, notlocalhost, so the OAuth state cookie domain matches the callback URL).Go to Account > External Authentication and click Connect next to GitHub. Complete the OAuth flow.
Create a workspace and SSH into it:
Flow 1: Token is valid — JSON output includes
expires_atInside the workspace, run:
Expected output (GitHub tokens have no expiry, so `expires_at` is the zero value):
{ "access_token": "<redacted>", "token_extra": null, "url": "", "type": "github", "expires_at": "0001-01-01T00:00:00Z", "username": "<redacted>", "password": "" }Flow 2: Token missing — JSON output includes auth URL, exit code 1
Disconnect GitHub in the Coder UI (Account > External Authentication > Disconnect), then inside the workspace run:
Expected output:
{ "access_token": "", "token_extra": null, "url": "http://127.0.0.1:3000/external-auth/github", "type": "", "expires_at": "0001-01-01T00:00:00Z", "username": "", "password": "" }