feat(auth): add OAuth and token-based login for OpenAI and Anthropic
Add `picoclaw auth` CLI command supporting: - OpenAI OAuth2 (PKCE + browser callback or device code flow) - Anthropic paste-token flow - Token storage at ~/.picoclaw/auth.json with 0600 permissions - Auto-refresh for expired OAuth tokens in provider Closes #18 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
112
pkg/auth/store.go
Normal file
112
pkg/auth/store.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuthCredential struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
AccountID string `json:"account_id,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
AuthMethod string `json:"auth_method"`
|
||||
}
|
||||
|
||||
type AuthStore struct {
|
||||
Credentials map[string]*AuthCredential `json:"credentials"`
|
||||
}
|
||||
|
||||
func (c *AuthCredential) IsExpired() bool {
|
||||
if c.ExpiresAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(c.ExpiresAt)
|
||||
}
|
||||
|
||||
func (c *AuthCredential) NeedsRefresh() bool {
|
||||
if c.ExpiresAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
return time.Now().Add(5 * time.Minute).After(c.ExpiresAt)
|
||||
}
|
||||
|
||||
func authFilePath() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".picoclaw", "auth.json")
|
||||
}
|
||||
|
||||
func LoadStore() (*AuthStore, error) {
|
||||
path := authFilePath()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &AuthStore{Credentials: make(map[string]*AuthCredential)}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var store AuthStore
|
||||
if err := json.Unmarshal(data, &store); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if store.Credentials == nil {
|
||||
store.Credentials = make(map[string]*AuthCredential)
|
||||
}
|
||||
return &store, nil
|
||||
}
|
||||
|
||||
func SaveStore(store *AuthStore) error {
|
||||
path := authFilePath()
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(store, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0600)
|
||||
}
|
||||
|
||||
func GetCredential(provider string) (*AuthCredential, error) {
|
||||
store, err := LoadStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cred, ok := store.Credentials[provider]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
func SetCredential(provider string, cred *AuthCredential) error {
|
||||
store, err := LoadStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.Credentials[provider] = cred
|
||||
return SaveStore(store)
|
||||
}
|
||||
|
||||
func DeleteCredential(provider string) error {
|
||||
store, err := LoadStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(store.Credentials, provider)
|
||||
return SaveStore(store)
|
||||
}
|
||||
|
||||
func DeleteAllCredentials() error {
|
||||
path := authFilePath()
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user