feat(oauth): Use keyring to store oauth token#1228
feat(oauth): Use keyring to store oauth token#1228burmudar wants to merge 14 commits intowb/add-oauth-refresh-tokenfrom
Conversation
|
This change is part of the following stack: Change managed by git-spice. |
2f24f44 to
7f2c665
Compare
278fc77 to
6f58c79
Compare
7f2c665 to
87b5732
Compare
6f58c79 to
5a355f7
Compare
- 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
Amp-Thread-ID: https://ampcode.com/threads/T-019bea47-5179-7418-86cf-bf1d4cc93d28 Co-authored-by: Amp <[email protected]> - minor keyring refactor
- 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
87b5732 to
abd0850
Compare
5a355f7 to
0f2e720
Compare
767025b to
64897ef
Compare
64897ef to
7978cce
Compare
| 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 { |
There was a problem hiding this comment.
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?
| 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) | ||
| } |
There was a problem hiding this comment.
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 |
| var ErrSecretNotFound = errors.New("secret not found") | ||
|
|
||
| // Store provides secure credential storage operations. | ||
| type Store interface { |
There was a problem hiding this comment.
where do you use this interface?
| } | ||
|
|
||
| // Put stores a key-value pair in the keyring. | ||
| func (k *keyringStore) Put(key string, data []byte) error { |
There was a problem hiding this comment.
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?
keegancsmith
left a comment
There was a problem hiding this comment.
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 |


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_TOKENdefined.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
src login --oauth https://sourcegraph.sourcegraph.comexport SRC_ENDPOINT=https://sourcegraph.sourcegraph.com; echo 'query { currentUser { username } }' | go run ./cmd/src apiSourcegraph CLIapplication and rerun previous commandPlatforms tested