Skip to content

Commit 1b551cb

Browse files
authored
feat(deps): upgrade sdk-go to v0.2.0 (credstore API) (#15)
- Upgrade the go-authgate SDK dependency to version 0.2.0 - Migrate token storage from the deprecated tokenstore package to the new generic credstore API - Update token persistence to be keyed by client ID when loading and saving credentials - Refactor runtime logic to explicitly track whether stored credentials exist instead of relying on nil tokens - Adjust device flow and token refresh functions to return value-based tokens and propagate errors consistently - Update API call flow to pass tokens by reference to support in-place refresh - Align tests and benchmarks with the new credential store interfaces and multi-client save semantics Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
1 parent 20810c2 commit 1b551cb

4 files changed

Lines changed: 66 additions & 57 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
charm.land/bubbletea/v2 v2.0.1
88
charm.land/lipgloss/v2 v2.0.0
99
github.com/appleboy/go-httpretry v0.11.0
10-
github.com/go-authgate/sdk-go v0.0.0-20260308143712-376f312901c8
10+
github.com/go-authgate/sdk-go v0.2.0
1111
github.com/google/uuid v1.6.0
1212
github.com/joho/godotenv v1.5.1
1313
golang.org/x/oauth2 v0.36.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMF
3232
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
3333
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3434
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35-
github.com/go-authgate/sdk-go v0.0.0-20260308143712-376f312901c8 h1:cqsgCsNlvRew75W5gzXyzZcdzqpvwMxX2AizrnsT01M=
36-
github.com/go-authgate/sdk-go v0.0.0-20260308143712-376f312901c8/go.mod h1:ZRyXFKqO8HqWXIAqIwhjSxJ0DE3RckTVn9UtlX7MvJ8=
35+
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
36+
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
3737
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
3838
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
3939
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=

main.go

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import (
2424

2525
tea "charm.land/bubbletea/v2"
2626
"github.com/go-authgate/device-cli/tui"
27-
"github.com/go-authgate/sdk-go/tokenstore"
27+
"github.com/go-authgate/sdk-go/credstore"
2828
)
2929

3030
var (
@@ -38,7 +38,7 @@ var (
3838
flagTokenStore *string
3939
configInitialized bool
4040
retryClient *retry.Client
41-
tokenStore tokenstore.Store
41+
tokenStore credstore.Store[credstore.Token]
4242
)
4343

4444
const defaultKeyringService = "authgate-device-cli"
@@ -185,16 +185,16 @@ func initConfig() {
185185
}
186186

187187
// Initialize token store based on mode
188-
fileStore := tokenstore.NewFileStore(tokenFile)
188+
fileStore := credstore.NewTokenFileStore(tokenFile)
189189
switch tokenStoreMode {
190190
case "file":
191191
tokenStore = fileStore
192192
case "keyring":
193-
tokenStore = tokenstore.NewKeyringStore(defaultKeyringService)
193+
tokenStore = credstore.NewTokenKeyringStore(defaultKeyringService)
194194
case "auto":
195-
kr := tokenstore.NewKeyringStore(defaultKeyringService)
196-
tokenStore = tokenstore.NewSecureStore(kr, fileStore)
197-
if !tokenStore.(*tokenstore.SecureStore).UseKeyring() {
195+
kr := credstore.NewTokenKeyringStore(defaultKeyringService)
196+
tokenStore = credstore.NewSecureStore[credstore.Token](kr, fileStore)
197+
if !tokenStore.(*credstore.SecureStore[credstore.Token]).UseKeyring() {
198198
fmt.Fprintln(
199199
os.Stderr,
200200
"⚠️ OS keyring unavailable, falling back to file-based token storage",
@@ -325,17 +325,22 @@ func run(ctx context.Context, d tui.Displayer) error {
325325
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
326326
defer stop()
327327

328-
var storage *tokenstore.Token
328+
var (
329+
storage credstore.Token
330+
hasStorage bool
331+
)
329332

330333
// Try to load existing tokens
331-
storage, err := tokenStore.Load(clientID)
334+
loaded, err := tokenStore.Load(clientID)
332335
switch {
333-
case err != nil && !errors.Is(err, tokenstore.ErrNotFound):
336+
case err != nil && !errors.Is(err, credstore.ErrNotFound):
334337
d.Fatal(err)
335338
return err
336339
case err != nil:
337340
d.TokensNotFound()
338-
case storage != nil:
341+
default:
342+
storage = loaded
343+
hasStorage = true
339344
d.TokensFound()
340345

341346
// Check if access token is still valid
@@ -349,18 +354,16 @@ func run(ctx context.Context, d tui.Displayer) error {
349354
newStorage, err := refreshAccessToken(ctx, storage.RefreshToken, d)
350355
if err != nil {
351356
d.RefreshFailed(err)
352-
storage = nil // Force device flow
357+
hasStorage = false // Force device flow
353358
} else {
354359
storage = newStorage
355360
d.RefreshOK()
356361
}
357362
}
358-
default:
359-
d.TokensNotFound()
360363
}
361364

362365
// If no valid tokens, do device flow
363-
if storage == nil {
366+
if !hasStorage {
364367
storage, err = performDeviceFlow(ctx, d)
365368
if err != nil {
366369
d.Fatal(err)
@@ -382,7 +385,7 @@ func run(ctx context.Context, d tui.Displayer) error {
382385
}
383386

384387
// Demonstrate automatic refresh on 401
385-
if err := makeAPICallWithAutoRefresh(ctx, storage, d); err != nil {
388+
if err := makeAPICallWithAutoRefresh(ctx, &storage, d); err != nil {
386389
// Check if error is due to expired refresh token
387390
if err == ErrRefreshTokenExpired {
388391
d.ReAuthRequired()
@@ -394,7 +397,7 @@ func run(ctx context.Context, d tui.Displayer) error {
394397

395398
// Retry API call with new tokens
396399
d.TokenRefreshedRetrying()
397-
if err := makeAPICallWithAutoRefresh(ctx, storage, d); err != nil {
400+
if err := makeAPICallWithAutoRefresh(ctx, &storage, d); err != nil {
398401
d.Fatal(err)
399402
return err
400403
}
@@ -473,7 +476,7 @@ func requestDeviceCode(ctx context.Context) (*oauth2.DeviceAuthResponse, error)
473476
}
474477

475478
// performDeviceFlow performs the OAuth device authorization flow
476-
func performDeviceFlow(ctx context.Context, d tui.Displayer) (*tokenstore.Token, error) {
479+
func performDeviceFlow(ctx context.Context, d tui.Displayer) (credstore.Token, error) {
477480
config := &oauth2.Config{
478481
ClientID: clientID,
479482
Endpoint: oauth2.Endpoint{
@@ -486,7 +489,7 @@ func performDeviceFlow(ctx context.Context, d tui.Displayer) (*tokenstore.Token,
486489
// Step 1: Request device code (with retry logic)
487490
deviceAuth, err := requestDeviceCode(ctx)
488491
if err != nil {
489-
return nil, fmt.Errorf("device code request failed: %w", err)
492+
return credstore.Token{}, fmt.Errorf("device code request failed: %w", err)
490493
}
491494

492495
d.DeviceCodeReady(
@@ -500,21 +503,21 @@ func performDeviceFlow(ctx context.Context, d tui.Displayer) (*tokenstore.Token,
500503
d.WaitingForAuth()
501504
token, err := pollForTokenWithProgress(ctx, config, deviceAuth, d)
502505
if err != nil {
503-
return nil, fmt.Errorf("token poll failed: %w", err)
506+
return credstore.Token{}, fmt.Errorf("token poll failed: %w", err)
504507
}
505508

506509
d.AuthSuccess()
507510

508511
// Convert to Token and save
509-
storage := &tokenstore.Token{
512+
storage := credstore.Token{
510513
AccessToken: token.AccessToken,
511514
RefreshToken: token.RefreshToken,
512515
TokenType: token.Type(),
513516
ExpiresAt: token.Expiry,
514517
ClientID: clientID,
515518
}
516519

517-
if err := tokenStore.Save(storage); err != nil {
520+
if err := tokenStore.Save(clientID, storage); err != nil {
518521
d.TokenSaveFailed(err)
519522
} else {
520523
d.TokenSaved(tokenStore.String())
@@ -715,7 +718,7 @@ func refreshAccessToken(
715718
ctx context.Context,
716719
refreshToken string,
717720
d tui.Displayer,
718-
) (*tokenstore.Token, error) {
721+
) (credstore.Token, error) {
719722
// Create request with timeout
720723
reqCtx, cancel := context.WithTimeout(ctx, refreshTokenTimeout)
721724
defer cancel()
@@ -732,38 +735,42 @@ func refreshAccessToken(
732735
strings.NewReader(data.Encode()),
733736
)
734737
if err != nil {
735-
return nil, fmt.Errorf("failed to create request: %w", err)
738+
return credstore.Token{}, fmt.Errorf("failed to create request: %w", err)
736739
}
737740
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
738741

739742
// Execute request with retry logic
740743
resp, err := retryClient.DoWithContext(reqCtx, req)
741744
if err != nil {
742-
return nil, fmt.Errorf("refresh request failed: %w", err)
745+
return credstore.Token{}, fmt.Errorf("refresh request failed: %w", err)
743746
}
744747
defer resp.Body.Close()
745748

746749
body, err := io.ReadAll(resp.Body)
747750
if err != nil {
748-
return nil, fmt.Errorf("failed to read response: %w", err)
751+
return credstore.Token{}, fmt.Errorf("failed to read response: %w", err)
749752
}
750753

751754
if resp.StatusCode != http.StatusOK {
752755
var errResp ErrorResponse
753756
if err := json.Unmarshal(body, &errResp); err == nil {
754757
// Check if refresh token is expired or invalid
755758
if errResp.Error == oauthErrInvalidGrant || errResp.Error == oauthErrInvalidToken {
756-
return nil, ErrRefreshTokenExpired
759+
return credstore.Token{}, ErrRefreshTokenExpired
757760
}
758-
return nil, fmt.Errorf("%s: %s", errResp.Error, errResp.ErrorDescription)
761+
return credstore.Token{}, fmt.Errorf("%s: %s", errResp.Error, errResp.ErrorDescription)
759762
}
760-
return nil, fmt.Errorf("refresh failed with status %d: %s", resp.StatusCode, string(body))
763+
return credstore.Token{}, fmt.Errorf(
764+
"refresh failed with status %d: %s",
765+
resp.StatusCode,
766+
string(body),
767+
)
761768
}
762769

763770
// Parse token response
764771
var tokenResp tokenResponse
765772
if err := json.Unmarshal(body, &tokenResp); err != nil {
766-
return nil, fmt.Errorf("failed to parse token response: %w", err)
773+
return credstore.Token{}, fmt.Errorf("failed to parse token response: %w", err)
767774
}
768775

769776
// Validate token response
@@ -772,7 +779,7 @@ func refreshAccessToken(
772779
tokenResp.TokenType,
773780
tokenResp.ExpiresIn,
774781
); err != nil {
775-
return nil, fmt.Errorf("invalid token response: %w", err)
782+
return credstore.Token{}, fmt.Errorf("invalid token response: %w", err)
776783
}
777784

778785
// Handle refresh token rotation modes:
@@ -784,7 +791,7 @@ func refreshAccessToken(
784791
newRefreshToken = refreshToken
785792
}
786793

787-
storage := &tokenstore.Token{
794+
storage := credstore.Token{
788795
AccessToken: tokenResp.AccessToken,
789796
RefreshToken: newRefreshToken,
790797
TokenType: tokenResp.TokenType,
@@ -793,7 +800,7 @@ func refreshAccessToken(
793800
}
794801

795802
// Save updated tokens
796-
if err := tokenStore.Save(storage); err != nil {
803+
if err := tokenStore.Save(clientID, storage); err != nil {
797804
d.TokenSaveFailed(err)
798805
}
799806

@@ -803,7 +810,7 @@ func refreshAccessToken(
803810
// makeAPICallWithAutoRefresh demonstrates automatic refresh on 401
804811
func makeAPICallWithAutoRefresh(
805812
ctx context.Context,
806-
storage *tokenstore.Token,
813+
storage *credstore.Token,
807814
d tui.Displayer,
808815
) error {
809816
// Try with current access token

0 commit comments

Comments
 (0)