smcli: refresh-token support - sessions no longer die after ~1h
The login grant requested offline_access but the issued refresh_token was parsed and discarded: every access token died with the ~1h Vaultwarden TTL, and consumers (all lanes) hit HTTP 401 on sync until a human re-logged in. - persist refresh_token in state (0600, same file) - add refresh grant (grant_type=refresh_token, rotated token saved) - on 401 for authed calls: refresh once, retry the request - persistTokens() keeps the rest of the state intact Build verified in golang:1.23-alpine (vet + gofmt clean). After deploy, one `sm login` issues a refresh token (~30d, rotated on use) and sessions self-heal from then on.
This commit is contained in:
+25
-12
@@ -30,14 +30,15 @@ import (
|
||||
const stateVersion = 1
|
||||
|
||||
type State struct {
|
||||
Version int `json:"version"`
|
||||
Server string `json:"server"`
|
||||
Email string `json:"email"`
|
||||
AccessToken string `json:"access_token"`
|
||||
KDFType int `json:"kdf_type"`
|
||||
KDFIter uint32 `json:"kdf_iter"`
|
||||
KDFMemory uint32 `json:"kdf_memory"`
|
||||
KDFParallel uint32 `json:"kdf_parallel"`
|
||||
Version int `json:"version"`
|
||||
Server string `json:"server"`
|
||||
Email string `json:"email"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
KDFType int `json:"kdf_type"`
|
||||
KDFIter uint32 `json:"kdf_iter"`
|
||||
KDFMemory uint32 `json:"kdf_memory"`
|
||||
KDFParallel uint32 `json:"kdf_parallel"`
|
||||
// MasterKey/StretchedKey/UserSymKey stored raw (hex) — file must be 0600.
|
||||
MasterKey string `json:"master_key"`
|
||||
StretchedKey string `json:"stretched_key"`
|
||||
@@ -54,6 +55,18 @@ func stateDir() string {
|
||||
|
||||
func statePath() string { return filepath.Join(stateDir(), "state.json") }
|
||||
|
||||
// persistTokens updates just the token pair in the existing state file
|
||||
// after a successful refresh (called from api.go refresh()).
|
||||
func persistTokens(c *Client) {
|
||||
s, err := loadState()
|
||||
if err != nil {
|
||||
return // no readable state; tokens stay in-memory for this run
|
||||
}
|
||||
s.AccessToken = c.AccessToken
|
||||
s.RefreshToken = c.RefreshToken
|
||||
_ = saveState(s)
|
||||
}
|
||||
|
||||
func saveState(s *State) error {
|
||||
if err := os.MkdirAll(stateDir(), 0o700); err != nil {
|
||||
return err
|
||||
@@ -83,8 +96,8 @@ func loadState() (*State, error) {
|
||||
func newClientFromState(s *State) (*Client, error) {
|
||||
c := &Client{
|
||||
Server: s.Server, Email: s.Email,
|
||||
AccessToken: s.AccessToken,
|
||||
KDFType: s.KDFType, KDFIter: s.KDFIter, KDFMemory: s.KDFMemory, KDFParallel: s.KDFParallel,
|
||||
AccessToken: s.AccessToken, RefreshToken: s.RefreshToken,
|
||||
KDFType: s.KDFType, KDFIter: s.KDFIter, KDFMemory: s.KDFMemory, KDFParallel: s.KDFParallel,
|
||||
HTTP: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
var err error
|
||||
@@ -108,8 +121,8 @@ func cmdLogin(server, email, password string) error {
|
||||
}
|
||||
s := &State{
|
||||
Version: stateVersion, Server: server, Email: email,
|
||||
AccessToken: c.AccessToken,
|
||||
KDFType: c.KDFType, KDFIter: c.KDFIter, KDFMemory: c.KDFMemory, KDFParallel: c.KDFParallel,
|
||||
AccessToken: c.AccessToken, RefreshToken: c.RefreshToken,
|
||||
KDFType: c.KDFType, KDFIter: c.KDFIter, KDFMemory: c.KDFMemory, KDFParallel: c.KDFParallel,
|
||||
MasterKey: toHex(c.MasterKey),
|
||||
StretchedKey: toHex(c.StretchedKey),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user