🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: expose external auth token expiry in agent API and CLI - #26883

Merged
BobbyHo merged 14 commits into
mainfrom
coder-26036
Jul 7, 2026
Merged

feat: expose external auth token expiry in agent API and CLI#26883
BobbyHo merged 14 commits into
mainfrom
coder-26036

Conversation

@BobbyHo

@BobbyHo BobbyHo commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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
  1. Create a GitHub OAuth app at https://github.com/settings/developers with:

    • Homepage URL: http://127.0.0.1:3000
    • Authorization callback URL: http://127.0.0.1:3000/external-auth/github/callback
  2. Start the dev server with the GitHub provider configured:

    CODER_EXTERNAL_AUTH_0_ID=github CODER_EXTERNAL_AUTH_0_TYPE=github CODER_EXTERNAL_AUTH_0_CLIENT_ID=<client-id> CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=<client-secret> ./scripts/develop.sh
  3. Log in at http://127.0.0.1:3000 (use 127.0.0.1, not localhost, so the OAuth state cookie domain matches the callback URL).

  4. Go to Account > External Authentication and click Connect next to GitHub. Complete the OAuth flow.

  5. Create a workspace and SSH into it:

    coder create test-workspace
    coder ssh test-workspace
Flow 1: Token is valid — JSON output includes expires_at

Inside the workspace, run:

coder external-auth access-token github --output json
echo "Exit code: $?"

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": ""
}
Exit code: 0
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:

coder external-auth access-token github --output json
echo "Exit code: $?"

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": ""
}
Exit code: 1

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.
@github-actions

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/reference/api/agents.md

@BobbyHo
BobbyHo marked this pull request as ready for review June 30, 2026 21:04
Comment thread coderd/workspaceagents_internal_test.go Outdated
Comment thread cli/externalauth.go Outdated
Comment thread cli/externalauth.go Outdated
Comment thread cli/externalauth_test.go Outdated
Comment thread coderd/workspaceagents_internal_test.go
Comment thread codersdk/agentsdk/agentsdk.go Outdated
BobbyHo and others added 8 commits July 6, 2026 16:36
…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.
@BobbyHo

BobbyHo commented Jul 7, 2026

Copy link
Copy Markdown
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!

@BobbyHo
BobbyHo requested a review from cstyan July 7, 2026 14:50

@cstyan cstyan 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.

LGTM, tests look much more concise than before.

@BobbyHo
BobbyHo merged commit b169f4d into main Jul 7, 2026
32 of 33 checks passed
@BobbyHo
BobbyHo deleted the coder-26036 branch July 7, 2026 19:38
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose external auth token expiry so git credential helpers can set password_expiry_utc

2 participants