Skip to content

Commit e6f2a1f

Browse files
fix(auth): recover from corrupt stored OAuth token
1 parent fc13b41 commit e6f2a1f

4 files changed

Lines changed: 80 additions & 3 deletions

File tree

internal/googleapi/client_auth.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ func tokenSourceForAccountScopesWithStoredScopeCheck(
236236
var tok secrets.Token
237237

238238
if t, err := store.GetToken(client, email); err != nil {
239-
if errors.Is(err, keyring.ErrKeyNotFound) {
239+
if errors.Is(err, keyring.ErrKeyNotFound) || errors.Is(err, secrets.ErrCorruptStoredToken) {
240240
return nil, &AuthRequiredError{Service: serviceLabel, Email: email, Client: client, Cause: err}
241241
}
242242

internal/googleapi/client_more_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,30 @@ func TestTokenSourceForAccountScopes_KeyNotFound(t *testing.T) {
166166
}
167167
}
168168

169+
func TestTokenSourceForAccountScopes_CorruptStoredToken(t *testing.T) {
170+
dependencies := tokenTestDependencies(func() (secrets.Store, error) {
171+
return &stubStore{err: fmt.Errorf("read token: %w", secrets.ErrCorruptStoredToken)}, nil
172+
})
173+
174+
_, err := tokenSourceForAccountScopesWithStoredScopeCheck(context.Background(), dependencies, "gmail", "[email protected]", "default", "id", "secret", []string{"s1"}, false)
175+
if err == nil {
176+
t.Fatalf("expected error")
177+
}
178+
var are *AuthRequiredError
179+
180+
if !errors.As(err, &are) {
181+
t.Fatalf("expected AuthRequiredError, got: %T %v", err, err)
182+
}
183+
184+
if are.Service != "gmail" || are.Email != "[email protected]" || are.Client != "default" {
185+
t.Fatalf("unexpected: %#v", are)
186+
}
187+
188+
if !errors.Is(are.Cause, secrets.ErrCorruptStoredToken) {
189+
t.Fatalf("expected corrupt stored token cause, got: %v", are.Cause)
190+
}
191+
}
192+
169193
func TestTokenSourceForAccountScopes_OtherGetError(t *testing.T) {
170194
dependencies := tokenTestDependencies(func() (secrets.Store, error) {
171195
return &stubStore{err: errNope}, nil

internal/secrets/store_more_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,55 @@ func TestGetTokenNoMigrateReadsLegacyWithoutWritingPrimary(t *testing.T) {
603603
}
604604
}
605605

606+
func TestGetTokenClassifiesCorruptStoredToken(t *testing.T) {
607+
ring := keyring.NewArrayKeyring(nil)
608+
store := &KeyringStore{ring: ring}
609+
client := "work"
610+
email := "[email protected]"
611+
612+
if err := ring.Set(keyringItem(tokenKey(client, email), []byte("{not-json"))); err != nil {
613+
t.Fatalf("seed token: %v", err)
614+
}
615+
616+
_, err := store.GetToken(client, email)
617+
if !errors.Is(err, ErrCorruptStoredToken) {
618+
t.Fatalf("expected corrupt stored token error, got %v", err)
619+
}
620+
621+
if errors.Is(err, keyring.ErrKeyNotFound) {
622+
t.Fatalf("corrupt present token should not look missing: %v", err)
623+
}
624+
}
625+
626+
func TestGetTokenMissingTokenNotCorrupt(t *testing.T) {
627+
store := &KeyringStore{ring: keyring.NewArrayKeyring(nil)}
628+
629+
_, err := store.GetToken("work", "[email protected]")
630+
if !errors.Is(err, keyring.ErrKeyNotFound) {
631+
t.Fatalf("expected missing token error, got %v", err)
632+
}
633+
634+
if errors.Is(err, ErrCorruptStoredToken) {
635+
t.Fatalf("missing token should not look corrupt: %v", err)
636+
}
637+
}
638+
639+
func TestGetTokenBySubjectClassifiesCorruptStoredToken(t *testing.T) {
640+
ring := keyring.NewArrayKeyring(nil)
641+
store := &KeyringStore{ring: ring}
642+
client := "work"
643+
subject := "sub-123"
644+
645+
if err := ring.Set(keyringItem(subjectTokenKey(client, subject), []byte("{not-json"))); err != nil {
646+
t.Fatalf("seed subject token: %v", err)
647+
}
648+
649+
_, err := store.getTokenBySubjectNoLock(client, subject)
650+
if !errors.Is(err, ErrCorruptStoredToken) {
651+
t.Fatalf("expected corrupt stored token error, got %v", err)
652+
}
653+
}
654+
606655
type legacyTokenReadErrorKeyring struct {
607656
*keyring.ArrayKeyring
608657
legacyKey string

internal/secrets/token.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ var (
2929
errMissingRefreshToken = errors.New("missing refresh token")
3030
)
3131

32+
// ErrCorruptStoredToken marks a present OAuth token entry whose JSON payload
33+
// cannot be decoded.
34+
var ErrCorruptStoredToken = errors.New("corrupt stored token")
35+
3236
type storedToken struct {
3337
RefreshToken string `json:"refresh_token"`
3438
Subject string `json:"subject,omitempty"`
@@ -187,7 +191,7 @@ func (s *KeyringStore) getTokenNoLockOptions(client string, email string, migrat
187191

188192
var st storedToken
189193
if err := json.Unmarshal(item.Data, &st); err != nil {
190-
return Token{}, fmt.Errorf("decode token: %w", err)
194+
return Token{}, fmt.Errorf("%w: decode token: %w", ErrCorruptStoredToken, err)
191195
}
192196

193197
return Token{
@@ -432,7 +436,7 @@ func (s *KeyringStore) getTokenBySubjectNoLock(client string, subject string) (T
432436

433437
var st storedToken
434438
if err := json.Unmarshal(item.Data, &st); err != nil {
435-
return Token{}, fmt.Errorf("decode token: %w", err)
439+
return Token{}, fmt.Errorf("%w: decode token: %w", ErrCorruptStoredToken, err)
436440
}
437441

438442
return Token{

0 commit comments

Comments
 (0)