package auth import ( "context" "encoding/json" "fmt" "log/slog" "github.com/bluesky-social/indigo/atproto/auth/oauth" "github.com/bluesky-social/indigo/atproto/syntax" ) const ( keyringService = "blup" currentSessionKey = "current-session" sessionKeyPrefix = "session:" authRequestPrefix = "auth-request:" pendingAuthStateKey = "pending-auth-state" loginIdentifierKey = "login-identifier" ) // KeyringAuthStore implements oauth.ClientAuthStore using the system keyring type KeyringAuthStore struct { keyring Keyring } // NewKeyringAuthStore creates a new KeyringAuthStore using the system keyring. func NewKeyringAuthStore() *KeyringAuthStore { return &KeyringAuthStore{keyring: DefaultKeyring} } // NewKeyringAuthStoreWithKeyring creates a KeyringAuthStore with a custom Keyring implementation. // This is useful for testing with a mock keyring. func NewKeyringAuthStoreWithKeyring(kr Keyring) *KeyringAuthStore { return &KeyringAuthStore{keyring: kr} } // sessionKey creates the keyring key for a session func sessionKey(did syntax.DID, sessionID string) string { return fmt.Sprintf("%s%s:%s", sessionKeyPrefix, did.String(), sessionID) } // GetSession retrieves a session from the keyring func (s *KeyringAuthStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { data, err := s.keyring.Get(keyringService, sessionKey(did, sessionID)) if err != nil { return nil, err } var sess oauth.ClientSessionData if err := json.Unmarshal([]byte(data), &sess); err != nil { return nil, err } return &sess, nil } // SaveSession stores a session in the keyring func (s *KeyringAuthStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error { slog.Debug("SaveSession called", "did", sess.AccountDID, "sessionID", sess.SessionID) data, err := json.Marshal(sess) if err != nil { slog.Error("SaveSession marshal failed", "err", err) return err } key := sessionKey(sess.AccountDID, sess.SessionID) slog.Debug("SaveSession saving", "key", key, "dataLen", len(data)) err = s.keyring.Set(keyringService, key, string(data)) if err != nil { slog.Error("SaveSession keyring.Set failed", "err", err) } else { slog.Debug("SaveSession success") } return err } // DeleteSession removes a session from the keyring func (s *KeyringAuthStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { return s.keyring.Delete(keyringService, sessionKey(did, sessionID)) } // authRequestKey creates the keyring key for an auth request func authRequestKey(state string) string { return fmt.Sprintf("%s%s", authRequestPrefix, state) } // GetAuthRequestInfo retrieves pending auth request info func (s *KeyringAuthStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { data, err := s.keyring.Get(keyringService, authRequestKey(state)) if err != nil { return nil, err } var info oauth.AuthRequestData if err := json.Unmarshal([]byte(data), &info); err != nil { return nil, err } return &info, nil } // SaveAuthRequestInfo stores pending auth request info func (s *KeyringAuthStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error { data, err := json.Marshal(info) if err != nil { return err } // Save the auth request data if err := s.keyring.Set(keyringService, authRequestKey(info.State), string(data)); err != nil { return err } // Also save the state as the current pending auth (for SSE correlation) return s.keyring.Set(keyringService, pendingAuthStateKey, info.State) } // GetPendingAuthState returns the state of the current pending auth request func (s *KeyringAuthStore) GetPendingAuthState() (string, error) { return s.keyring.Get(keyringService, pendingAuthStateKey) } // ClearPendingAuthState removes the pending auth state func (s *KeyringAuthStore) ClearPendingAuthState() error { return s.keyring.Delete(keyringService, pendingAuthStateKey) } // DeleteAuthRequestInfo removes pending auth request info func (s *KeyringAuthStore) DeleteAuthRequestInfo(ctx context.Context, state string) error { return s.keyring.Delete(keyringService, authRequestKey(state)) } // CurrentSessionRef stores reference to the current active session type CurrentSessionRef struct { DID string `json:"did"` SessionID string `json:"session_id"` } // GetCurrentSession retrieves the current active session for the CLI func (s *KeyringAuthStore) GetCurrentSession(ctx context.Context) (*oauth.ClientSessionData, error) { refData, err := s.keyring.Get(keyringService, currentSessionKey) if err != nil { return nil, err } var ref CurrentSessionRef if err := json.Unmarshal([]byte(refData), &ref); err != nil { return nil, err } did, err := syntax.ParseDID(ref.DID) if err != nil { return nil, err } return s.GetSession(ctx, did, ref.SessionID) } // SetCurrentSession sets the current active session reference func (s *KeyringAuthStore) SetCurrentSession(ctx context.Context, sess *oauth.ClientSessionData) error { slog.Debug("SetCurrentSession called", "did", sess.AccountDID, "sessionID", sess.SessionID) ref := CurrentSessionRef{ DID: sess.AccountDID.String(), SessionID: sess.SessionID, } data, err := json.Marshal(ref) if err != nil { slog.Error("SetCurrentSession marshal failed", "err", err) return err } slog.Debug("SetCurrentSession saving", "data", string(data)) err = s.keyring.Set(keyringService, currentSessionKey, string(data)) if err != nil { slog.Error("SetCurrentSession keyring.Set failed", "err", err) } else { slog.Debug("SetCurrentSession success") } return err } // ClearCurrentSession removes the current session reference func (s *KeyringAuthStore) ClearCurrentSession() error { return s.keyring.Delete(keyringService, currentSessionKey) } // GetLoginIdentifier retrieves the stored login identifier (handle or PDS URL) func (s *KeyringAuthStore) GetLoginIdentifier() (string, error) { return s.keyring.Get(keyringService, loginIdentifierKey) } // SetLoginIdentifier stores the login identifier for re-authentication func (s *KeyringAuthStore) SetLoginIdentifier(id string) error { return s.keyring.Set(keyringService, loginIdentifierKey, id) } // ClearLoginIdentifier removes the stored login identifier func (s *KeyringAuthStore) ClearLoginIdentifier() error { return s.keyring.Delete(keyringService, loginIdentifierKey) }