Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ SCOPE=read write

# Token storage
TOKEN_FILE=.authgate-tokens.json
# Token storage backend: auto (default), file, keyring
# auto = use OS keyring if available, fallback to TOKEN_FILE
TOKEN_STORE=auto
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
module github.com/go-authgate/oauth-cli

go 1.24.2
go 1.25.0

require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
charm.land/lipgloss/v2 v2.0.0
github.com/appleboy/go-httpretry v0.11.0
github.com/go-authgate/sdk-go v0.0.0-20260308143712-376f312901c8
github.com/go-authgate/sdk-go v0.2.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
)

require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
al.essio.dev/pkg/shellescape v1.6.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
Expand All @@ -22,14 +22,14 @@ require (
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/zalando/go-keyring v0.2.6 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/sys v0.42.0 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
Expand Down Expand Up @@ -30,12 +32,18 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-authgate/sdk-go v0.0.0-20260308143712-376f312901c8 h1:cqsgCsNlvRew75W5gzXyzZcdzqpvwMxX2AizrnsT01M=
github.com/go-authgate/sdk-go v0.0.0-20260308143712-376f312901c8/go.mod h1:ZRyXFKqO8HqWXIAqIwhjSxJ0DE3RckTVn9UtlX7MvJ8=
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down Expand Up @@ -66,5 +74,7 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
37 changes: 22 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"time"

"github.com/go-authgate/oauth-cli/tui"
"github.com/go-authgate/sdk-go/tokenstore"
"github.com/go-authgate/sdk-go/credstore"

tea "charm.land/bubbletea/v2"
retry "github.com/appleboy/go-httpretry"
Expand All @@ -36,7 +36,7 @@ var (
scope string
tokenFile string
tokenStoreMode string
tokenStore tokenstore.Store
tokenStore credstore.Store[credstore.Token]
configInitialized bool
retryClient *retry.Client
configWarnings []string
Expand Down Expand Up @@ -180,23 +180,24 @@ func initConfig() {

// initTokenStore creates a token store based on the given mode.
// It returns the store, any warnings, and an error if the mode is invalid.
func initTokenStore(mode, filePath, keyringService string) (tokenstore.Store, []string, error) {
fileStore := tokenstore.NewFileStore(filePath)
func initTokenStore(
mode, filePath, keyringService string,
) (credstore.Store[credstore.Token], []string, error) {
fileStore := credstore.NewTokenFileStore(filePath)
var warnings []string

switch mode {
case "file":
return fileStore, nil, nil
case "keyring":
return tokenstore.NewKeyringStore(keyringService), nil, nil
return credstore.NewTokenKeyringStore(keyringService), nil, nil
case "auto":
kr := tokenstore.NewKeyringStore(keyringService)
store := tokenstore.NewSecureStore(kr, fileStore)
if !store.UseKeyring() {
ss := credstore.DefaultTokenSecureStore(keyringService, filePath)
if !ss.UseKeyring() {
warnings = append(warnings,
"OS keyring unavailable, falling back to file-based token storage")
}
return store, warnings, nil
return ss, warnings, nil
default:
return nil, nil, fmt.Errorf(
"invalid token-store value: %s (must be auto, file, or keyring)",
Expand Down Expand Up @@ -558,15 +559,19 @@ func main() {

deps := tui.Deps{
LoadTokens: func() (*tui.TokenStorage, error) {
return tokenStore.Load(clientID)
tok, err := tokenStore.Load(clientID)
if err != nil {
return nil, err
}
return &tok, nil
},
RefreshToken: func(ctx context.Context, refreshToken string) (*tui.TokenStorage, string, error) {
storage, err := refreshAccessToken(ctx, refreshToken)
if err != nil {
return nil, "", err
}
saveWarning := ""
if saveErr := tokenStore.Save(storage); saveErr != nil {
if saveErr := tokenStore.Save(storage.ClientID, *storage); saveErr != nil {
saveWarning = fmt.Sprintf("Warning: Failed to save refreshed tokens: %v", saveErr)
}
return storage, saveWarning, nil
Expand All @@ -577,10 +582,12 @@ func main() {
OpenBrowser: openBrowser,
StartCallback: startCallbackServer,
ExchangeCode: exchangeCode,
SaveTokens: tokenStore.Save,
VerifyToken: verifyToken,
MakeAPICall: makeAPICallWithAutoRefresh,
CallbackPort: callbackPort,
SaveTokens: func(storage *tui.TokenStorage) error {
return tokenStore.Save(storage.ClientID, *storage)
},
VerifyToken: verifyToken,
MakeAPICall: makeAPICallWithAutoRefresh,
CallbackPort: callbackPort,
}

p := tea.NewProgram(
Expand Down
24 changes: 12 additions & 12 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"time"

"github.com/go-authgate/oauth-cli/tui"
"github.com/go-authgate/sdk-go/tokenstore"
"github.com/go-authgate/sdk-go/credstore"
)

func TestValidateServerURL(t *testing.T) {
Expand Down Expand Up @@ -82,18 +82,18 @@ func TestValidateTokenResponse(t *testing.T) {

func TestSaveAndLoadTokens(t *testing.T) {
// Use a non-existent path so FileStore starts fresh (empty file causes JSON parse error).
store := tokenstore.NewFileStore(filepath.Join(t.TempDir(), "tokens.json"))
store := credstore.NewTokenFileStore(filepath.Join(t.TempDir(), "tokens.json"))
const testClientID = "test-client-id"

token := &tokenstore.Token{
token := credstore.Token{
AccessToken: "access-token-value",
RefreshToken: "refresh-token-value",
TokenType: "Bearer",
ExpiresAt: time.Now().Add(time.Hour).UTC().Truncate(time.Second),
ClientID: testClientID,
}

if err := store.Save(token); err != nil {
if err := store.Save(testClientID, token); err != nil {
t.Fatalf("store.Save() error: %v", err)
}

Expand All @@ -114,11 +114,11 @@ func TestSaveAndLoadTokens(t *testing.T) {
}

func TestSaveTokens_MultipleClients(t *testing.T) {
store := tokenstore.NewFileStore(filepath.Join(t.TempDir(), "tokens-multi.json"))
store := credstore.NewTokenFileStore(filepath.Join(t.TempDir(), "tokens-multi.json"))

// Save tokens for two different clients.
for _, id := range []string{"client-a", "client-b"} {
if err := store.Save(&tokenstore.Token{
if err := store.Save(id, credstore.Token{
AccessToken: "token-" + id,
RefreshToken: "refresh-" + id,
TokenType: "Bearer",
Expand Down Expand Up @@ -210,8 +210,8 @@ func TestInitTokenStore_File(t *testing.T) {
if len(warnings) != 0 {
t.Errorf("expected no warnings, got %v", warnings)
}
if _, ok := store.(*tokenstore.FileStore); !ok {
t.Errorf("expected *tokenstore.FileStore, got %T", store)
if _, ok := store.(*credstore.FileStore[credstore.Token]); !ok {
t.Errorf("expected *credstore.FileStore[credstore.Token], got %T", store)
}
}

Expand All @@ -227,8 +227,8 @@ func TestInitTokenStore_Keyring(t *testing.T) {
if len(warnings) != 0 {
t.Errorf("expected no warnings, got %v", warnings)
}
if _, ok := store.(*tokenstore.KeyringStore); !ok {
t.Errorf("expected *tokenstore.KeyringStore, got %T", store)
if _, ok := store.(*credstore.KeyringStore[credstore.Token]); !ok {
t.Errorf("expected *credstore.KeyringStore[credstore.Token], got %T", store)
}
}

Expand All @@ -241,9 +241,9 @@ func TestInitTokenStore_Auto(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
secureStore, ok := store.(*tokenstore.SecureStore)
secureStore, ok := store.(*credstore.SecureStore[credstore.Token])
if !ok {
t.Fatalf("expected *tokenstore.SecureStore, got %T", store)
t.Fatalf("expected *credstore.SecureStore[credstore.Token], got %T", store)
}
// In CI / test environments the OS keyring is typically unavailable,
// so we expect the fallback warning. On systems with a keyring the
Expand Down
4 changes: 2 additions & 2 deletions tui/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package tui
import (
"errors"

"github.com/go-authgate/sdk-go/tokenstore"
"github.com/go-authgate/sdk-go/credstore"
)

// ErrRefreshTokenExpired indicates the refresh token has expired or is invalid.
var ErrRefreshTokenExpired = errors.New("refresh token expired or invalid")

// TokenStorage holds persisted OAuth tokens for one client.
type TokenStorage = tokenstore.Token
type TokenStorage = credstore.Token

// PKCEParams holds the code verifier and challenge for PKCE (RFC 7636).
type PKCEParams struct {
Expand Down
Loading