mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 13:17:09 +00:00
237 lines
6.0 KiB
Go
237 lines
6.0 KiB
Go
package oauth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
|
)
|
|
|
|
// FileStore implements oauth.ClientAuthStore with file-based persistence
|
|
type FileStore struct {
|
|
path string
|
|
sessions map[string]*oauth.ClientSessionData // Key: "did:sessionID"
|
|
requests map[string]*oauth.AuthRequestData // Key: state
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// FileStoreData represents the JSON structure stored on disk
|
|
type FileStoreData struct {
|
|
Sessions map[string]*oauth.ClientSessionData `json:"sessions"`
|
|
Requests map[string]*oauth.AuthRequestData `json:"requests"`
|
|
}
|
|
|
|
// NewFileStore creates a new file-based OAuth store
|
|
func NewFileStore(path string) (*FileStore, error) {
|
|
store := &FileStore{
|
|
path: path,
|
|
sessions: make(map[string]*oauth.ClientSessionData),
|
|
requests: make(map[string]*oauth.AuthRequestData),
|
|
}
|
|
|
|
// Load existing data if file exists
|
|
if err := store.load(); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("failed to load store: %w", err)
|
|
}
|
|
// File doesn't exist yet, that's ok
|
|
}
|
|
|
|
return store, nil
|
|
}
|
|
|
|
// GetDefaultStorePath returns the default storage path for OAuth data
|
|
func GetDefaultStorePath() (string, error) {
|
|
// For AppView: /var/lib/atcr/oauth-sessions.json
|
|
// For CLI tools: ~/.atcr/oauth-sessions.json
|
|
|
|
// Check if running as a service (has write access to /var/lib)
|
|
servicePath := "/var/lib/atcr/oauth-sessions.json"
|
|
if err := os.MkdirAll(filepath.Dir(servicePath), 0700); err == nil {
|
|
// Can write to /var/lib, use service path
|
|
return servicePath, nil
|
|
}
|
|
|
|
// Fall back to user home directory
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get home directory: %w", err)
|
|
}
|
|
|
|
atcrDir := filepath.Join(homeDir, ".atcr")
|
|
if err := os.MkdirAll(atcrDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to create .atcr directory: %w", err)
|
|
}
|
|
|
|
return filepath.Join(atcrDir, "oauth-sessions.json"), nil
|
|
}
|
|
|
|
// GetSession retrieves a session by DID and session ID
|
|
func (s *FileStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
key := makeSessionKey(did.String(), sessionID)
|
|
session, ok := s.sessions[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("session not found: %s/%s", did, sessionID)
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
// SaveSession saves or updates a session (upsert)
|
|
func (s *FileStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
key := makeSessionKey(sess.AccountDID.String(), sess.SessionID)
|
|
s.sessions[key] = &sess
|
|
|
|
return s.save()
|
|
}
|
|
|
|
// DeleteSession removes a session
|
|
func (s *FileStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
key := makeSessionKey(did.String(), sessionID)
|
|
delete(s.sessions, key)
|
|
|
|
return s.save()
|
|
}
|
|
|
|
// GetAuthRequestInfo retrieves authentication request data by state
|
|
func (s *FileStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
request, ok := s.requests[state]
|
|
if !ok {
|
|
return nil, fmt.Errorf("auth request not found: %s", state)
|
|
}
|
|
|
|
return request, nil
|
|
}
|
|
|
|
// SaveAuthRequestInfo saves authentication request data
|
|
func (s *FileStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.requests[info.State] = &info
|
|
|
|
return s.save()
|
|
}
|
|
|
|
// DeleteAuthRequestInfo removes authentication request data
|
|
func (s *FileStore) DeleteAuthRequestInfo(ctx context.Context, state string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
delete(s.requests, state)
|
|
|
|
return s.save()
|
|
}
|
|
|
|
// CleanupExpired removes expired sessions and auth requests
|
|
// Should be called periodically (e.g., every hour)
|
|
func (s *FileStore) CleanupExpired() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
modified := false
|
|
|
|
// Clean up auth requests older than 10 minutes
|
|
// (OAuth flows should complete quickly)
|
|
for state := range s.requests {
|
|
// Note: AuthRequestData doesn't have a timestamp in indigo's implementation
|
|
// For now, we'll rely on the OAuth server's cleanup routine
|
|
// or we could extend AuthRequestData with metadata
|
|
_ = state // Placeholder for future expiration logic
|
|
}
|
|
|
|
// Sessions don't have expiry in the data structure
|
|
// Cleanup would need to be token-based (check token expiry)
|
|
// For now, manual cleanup via DeleteSession
|
|
_ = now
|
|
|
|
if modified {
|
|
return s.save()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListSessions returns all stored sessions for debugging/management
|
|
func (s *FileStore) ListSessions() map[string]*oauth.ClientSessionData {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
// Return a copy to prevent external modification
|
|
result := make(map[string]*oauth.ClientSessionData)
|
|
maps.Copy(result, s.sessions)
|
|
return result
|
|
}
|
|
|
|
// load reads data from disk
|
|
func (s *FileStore) load() error {
|
|
data, err := os.ReadFile(s.path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var storeData FileStoreData
|
|
if err := json.Unmarshal(data, &storeData); err != nil {
|
|
return fmt.Errorf("failed to parse store: %w", err)
|
|
}
|
|
|
|
if storeData.Sessions != nil {
|
|
s.sessions = storeData.Sessions
|
|
}
|
|
if storeData.Requests != nil {
|
|
s.requests = storeData.Requests
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// save writes data to disk
|
|
func (s *FileStore) save() error {
|
|
storeData := FileStoreData{
|
|
Sessions: s.sessions,
|
|
Requests: s.requests,
|
|
}
|
|
|
|
data, err := json.MarshalIndent(storeData, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal store: %w", err)
|
|
}
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil {
|
|
return fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
// Write with restrictive permissions
|
|
if err := os.WriteFile(s.path, data, 0600); err != nil {
|
|
return fmt.Errorf("failed to write store: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// makeSessionKey creates a composite key for session storage
|
|
func makeSessionKey(did, sessionID string) string {
|
|
return fmt.Sprintf("%s:%s", did, sessionID)
|
|
}
|