Skip to content

feat(oauth): Use keyring to store oauth token#1228

Merged
burmudar merged 17 commits intowb/add-oauth-refresh-tokenfrom
wb/oauth-keyring
Mar 9, 2026
Merged

feat(oauth): Use keyring to store oauth token#1228
burmudar merged 17 commits intowb/add-oauth-refresh-tokenfrom
wb/oauth-keyring

Conversation

@burmudar
Copy link
Contributor

@burmudar burmudar commented Dec 8, 2025

This adds a keyring which is used to store OAuth credentials. The OAuth credentials are retrieved from the keyring whenever there is no SRC_ACCESS_TOKEN defined.

When we use an OAuth token, we also add a OAuth transport which will automatically refresh the access token using the refresh token when it is near to expiring.

Credentials are stored in the keyring with under the service "Sourcegraph CLI" and with key "src:oauth:". The reason for the endpoint being in the key name is that some clients might access multiple sourcegraph instances like dev and prod, and we can't reuse the same credentials for the different servers.

Test plan

  • Authorize by running src login --oauth https://sourcegraph.sourcegraph.com
  • run export SRC_ENDPOINT=https://sourcegraph.sourcegraph.com; echo 'query { currentUser { username } }' | go run ./cmd/src api
  • Revoke access of Sourcegraph CLI application and rerun previous command
The OAuth token is invalid. Please check that the Sourcegraph CLI client is still authorized.

To re-authorize, run: src login

Learn more at https://github.com/sourcegraph/src-cli#readme

error: 401 Unauthorized

Invalid OAuth token.
exit status 1

Platforms tested

  • Mac
  • Windows
  • Linux

@burmudar
Copy link
Contributor Author

burmudar commented Dec 8, 2025

@burmudar burmudar force-pushed the wb/add-oauth-refresh-token branch from 2f24f44 to 7f2c665 Compare January 23, 2026 09:03
@burmudar burmudar force-pushed the wb/add-oauth-refresh-token branch from 7f2c665 to 87b5732 Compare January 23, 2026 09:10
@burmudar burmudar force-pushed the wb/add-oauth-refresh-token branch from 87b5732 to abd0850 Compare February 26, 2026 12:33
@burmudar burmudar self-assigned this Feb 26, 2026
@burmudar burmudar requested review from a team, eseliger and keegancsmith February 26, 2026 13:58
@burmudar burmudar force-pushed the wb/oauth-keyring branch 2 times, most recently from 64897ef to 7978cce Compare February 26, 2026 14:05
@burmudar burmudar marked this pull request as ready for review February 26, 2026 14:08
}

// Put stores a key-value pair in the keyring.
func (k *keyringStore) Put(key string, data []byte) error {
Copy link
Member

Choose a reason for hiding this comment

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

given the underlying library is all just global function calls, why don't we just do the same and wrap them with the service name we use?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My main motivation to keep a struct is to avoid making Get / Put param lists be "a lot" e.g.
Put(ctx context.Context, endpoint, key string, data []byte)
Get(ctx context.Context, endpoint, key string)

We also centralize the store name to only happen once Open is called. Once it is open, the user doesn't have to keep extra context about the endpoint and can just use the store.

Copy link
Member

@keegancsmith keegancsmith left a comment

Choose a reason for hiding this comment

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

My main concern is hitting libsecret/etc when we don't need to. But the underlying library looks good + we only open the keyring when doing oauth. So LGTM

burmudar added 6 commits March 9, 2026 11:13
- rename keyring to store
- make keyring struct src-cli and set label on secret
- create token struct from TokenResponse
- Token converts expiresIn to a timestamp
- Store the token with the endpoint suffix
- OAuth transport and use when available in api client
- Add secret store that supports different backends
- We use a registry map for a few secrets and the registry gets
  persisted as one secret to the keyring. We don't waant to create a
  keyring secret for every different secret
- Store is opened once to load the registry.
- use secretStorage to store oauth tokens
- use token.ClientID during refresh
- best effort store refresh token
burmudar added 9 commits March 9, 2026 11:13
handle oauth discovery failure and set client id on token

- use SRC CLI client id as default and handle discovery failures
- add clientID flag and set it on the token

improve error message and panic in apiClient if no usable token

- warn if we fail to store the token on login
- panic if apiClient has no accessToken or OAuth token to use
- remove panic
- use lib/errors from sg
- remove delete operation
- remove store interface
@burmudar burmudar force-pushed the wb/add-oauth-refresh-token branch from abd0850 to f79c981 Compare March 9, 2026 09:30
burmudar added 2 commits March 9, 2026 11:39
- add mutex to guard concurrent changes of token
- pull refreshing of token out into `refreshToken`
- additional comments
@burmudar burmudar merged commit 88d56e9 into wb/add-oauth-refresh-token Mar 9, 2026
10 checks passed
@burmudar burmudar deleted the wb/oauth-keyring branch March 9, 2026 09:52
burmudar added a commit that referenced this pull request Mar 9, 2026
* add refresh to oauthdevice.Client
* oauthdevice: add RefreshToken field and Refresh method

* feat(oauth): Use keyring to store oauth token (#1228)
  * add refresh to oauthdevice.Client
  * add OAuth Transport and use it if no access token
  * secrets: switch to zalando/go-keyring and add context support
  * secrets: scope keyring by endpoint
burmudar added a commit that referenced this pull request Mar 9, 2026
…Auth client (#1223)

* removed unused func

* add refresh token to device response unmarshall

* make NewClient take ClientID as param

* add oauth flow and use oauth token when SRC_ACCESS_TOKEN is empty

* feat(oauth): Add refresh to oauthdevice.Client (#1227)

* add refresh to oauthdevice.Client
* oauthdevice: add RefreshToken field and Refresh method

* feat(oauth): Use keyring to store oauth token (#1228)
  * add refresh to oauthdevice.Client
  * add OAuth Transport and use it if no access token
  * secrets: switch to zalando/go-keyring and add context support
  * secrets: scope keyring by endpoint
burmudar added a commit that referenced this pull request Mar 23, 2026
…Auth client (#1223)

* removed unused func

* add refresh token to device response unmarshall

* make NewClient take ClientID as param

* add oauth flow and use oauth token when SRC_ACCESS_TOKEN is empty

* feat(oauth): Add refresh to oauthdevice.Client (#1227)

* add refresh to oauthdevice.Client
* oauthdevice: add RefreshToken field and Refresh method

* feat(oauth): Use keyring to store oauth token (#1228)
  * add refresh to oauthdevice.Client
  * add OAuth Transport and use it if no access token
  * secrets: switch to zalando/go-keyring and add context support
  * secrets: scope keyring by endpoint

(cherry picked from commit 0bc535e)
burmudar added a commit that referenced this pull request Mar 23, 2026
…Auth client (#1223)

* removed unused func

* add refresh token to device response unmarshall

* make NewClient take ClientID as param

* add oauth flow and use oauth token when SRC_ACCESS_TOKEN is empty

* feat(oauth): Add refresh to oauthdevice.Client (#1227)

* add refresh to oauthdevice.Client
* oauthdevice: add RefreshToken field and Refresh method

* feat(oauth): Use keyring to store oauth token (#1228)
  * add refresh to oauthdevice.Client
  * add OAuth Transport and use it if no access token
  * secrets: switch to zalando/go-keyring and add context support
  * secrets: scope keyring by endpoint

(cherry picked from commit 0bc535e)
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