Skip to content

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

Open
burmudar wants to merge 14 commits intowb/add-oauth-refresh-tokenfrom
wb/oauth-keyring
Open

feat(oauth): Use keyring to store oauth token#1228
burmudar wants to merge 14 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
- 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
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
@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 marked this pull request as ready for review February 26, 2026 14:08
printProblem("No access token is configured.")
case endpointConflict:
printProblem(fmt.Sprintf("The configured endpoint is %s, not %s.", cfg.Endpoint, endpointArg))
if err := oauth.StoreToken(ctx, token); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

opted to not store anything when failing to store in the keyring. This means if we fail storing the user would have to login everytime - not exactly ideal.

We could write the details to disk and warn the user? And everytime we load it we warn them again?

Comment on lines +25 to +34
prevToken := t.Token
token, err := maybeRefresh(ctx, t.Token)
if err != nil {
return nil, err
}
t.Token = token
if token != prevToken {
// try to save the token if we fail let the request continue with in memory token
_ = storeRefreshedTokenFn(ctx, token)
}
Copy link
Member

@keegancsmith keegancsmith Mar 2, 2026

Choose a reason for hiding this comment

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

This should all be behind a mutex to prevent concurrent refresh attempts. Additionally then when you read t.Token below that should have come from this critical section.

To make the code easy to read I suggest pulling this out into a method on *Transport like getToken(ctx) then you can just have a simple mu.Lock defer mu.Unlock at the top.

type Store interface {
Get(key string) ([]byte, error)
Put(key string, data []byte) error
Delete(key string) error
Copy link
Member

Choose a reason for hiding this comment

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

no one calls this

var ErrSecretNotFound = errors.New("secret not found")

// Store provides secure credential storage operations.
type Store interface {
Copy link
Member

Choose a reason for hiding this comment

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

where do you use this interface?

}

// 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
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


// Open opens the system keyring for the Sourcegraph CLI.
func Open(ctx context.Context) (Store, error) {
return &keyringStore{ctx: ctx, serviceName: "Sourcegraph CLI"}, nil
Copy link
Member

Choose a reason for hiding this comment

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

I think our service name needs to include the endpoint we are authenticating against. eg this is what it looks like when I look at some of my secrets

image.png

image.png

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