Upload images to your PDS and get instant CDN URLs via images.blue
at main 202 lines 6.4 kB view raw
1package auth 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 9 "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 "github.com/bluesky-social/indigo/atproto/syntax" 11) 12 13const ( 14 keyringService = "blup" 15 currentSessionKey = "current-session" 16 sessionKeyPrefix = "session:" 17 authRequestPrefix = "auth-request:" 18 pendingAuthStateKey = "pending-auth-state" 19 loginIdentifierKey = "login-identifier" 20) 21 22// KeyringAuthStore implements oauth.ClientAuthStore using the system keyring 23type KeyringAuthStore struct { 24 keyring Keyring 25} 26 27// NewKeyringAuthStore creates a new KeyringAuthStore using the system keyring. 28func NewKeyringAuthStore() *KeyringAuthStore { 29 return &KeyringAuthStore{keyring: DefaultKeyring} 30} 31 32// NewKeyringAuthStoreWithKeyring creates a KeyringAuthStore with a custom Keyring implementation. 33// This is useful for testing with a mock keyring. 34func NewKeyringAuthStoreWithKeyring(kr Keyring) *KeyringAuthStore { 35 return &KeyringAuthStore{keyring: kr} 36} 37 38// sessionKey creates the keyring key for a session 39func sessionKey(did syntax.DID, sessionID string) string { 40 return fmt.Sprintf("%s%s:%s", sessionKeyPrefix, did.String(), sessionID) 41} 42 43// GetSession retrieves a session from the keyring 44func (s *KeyringAuthStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { 45 data, err := s.keyring.Get(keyringService, sessionKey(did, sessionID)) 46 if err != nil { 47 return nil, err 48 } 49 50 var sess oauth.ClientSessionData 51 if err := json.Unmarshal([]byte(data), &sess); err != nil { 52 return nil, err 53 } 54 55 return &sess, nil 56} 57 58// SaveSession stores a session in the keyring 59func (s *KeyringAuthStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error { 60 slog.Debug("SaveSession called", "did", sess.AccountDID, "sessionID", sess.SessionID) 61 data, err := json.Marshal(sess) 62 if err != nil { 63 slog.Error("SaveSession marshal failed", "err", err) 64 return err 65 } 66 67 key := sessionKey(sess.AccountDID, sess.SessionID) 68 slog.Debug("SaveSession saving", "key", key, "dataLen", len(data)) 69 err = s.keyring.Set(keyringService, key, string(data)) 70 if err != nil { 71 slog.Error("SaveSession keyring.Set failed", "err", err) 72 } else { 73 slog.Debug("SaveSession success") 74 } 75 return err 76} 77 78// DeleteSession removes a session from the keyring 79func (s *KeyringAuthStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { 80 return s.keyring.Delete(keyringService, sessionKey(did, sessionID)) 81} 82 83// authRequestKey creates the keyring key for an auth request 84func authRequestKey(state string) string { 85 return fmt.Sprintf("%s%s", authRequestPrefix, state) 86} 87 88// GetAuthRequestInfo retrieves pending auth request info 89func (s *KeyringAuthStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { 90 data, err := s.keyring.Get(keyringService, authRequestKey(state)) 91 if err != nil { 92 return nil, err 93 } 94 95 var info oauth.AuthRequestData 96 if err := json.Unmarshal([]byte(data), &info); err != nil { 97 return nil, err 98 } 99 100 return &info, nil 101} 102 103// SaveAuthRequestInfo stores pending auth request info 104func (s *KeyringAuthStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error { 105 data, err := json.Marshal(info) 106 if err != nil { 107 return err 108 } 109 110 // Save the auth request data 111 if err := s.keyring.Set(keyringService, authRequestKey(info.State), string(data)); err != nil { 112 return err 113 } 114 115 // Also save the state as the current pending auth (for SSE correlation) 116 return s.keyring.Set(keyringService, pendingAuthStateKey, info.State) 117} 118 119// GetPendingAuthState returns the state of the current pending auth request 120func (s *KeyringAuthStore) GetPendingAuthState() (string, error) { 121 return s.keyring.Get(keyringService, pendingAuthStateKey) 122} 123 124// ClearPendingAuthState removes the pending auth state 125func (s *KeyringAuthStore) ClearPendingAuthState() error { 126 return s.keyring.Delete(keyringService, pendingAuthStateKey) 127} 128 129// DeleteAuthRequestInfo removes pending auth request info 130func (s *KeyringAuthStore) DeleteAuthRequestInfo(ctx context.Context, state string) error { 131 return s.keyring.Delete(keyringService, authRequestKey(state)) 132} 133 134// CurrentSessionRef stores reference to the current active session 135type CurrentSessionRef struct { 136 DID string `json:"did"` 137 SessionID string `json:"session_id"` 138} 139 140// GetCurrentSession retrieves the current active session for the CLI 141func (s *KeyringAuthStore) GetCurrentSession(ctx context.Context) (*oauth.ClientSessionData, error) { 142 refData, err := s.keyring.Get(keyringService, currentSessionKey) 143 if err != nil { 144 return nil, err 145 } 146 147 var ref CurrentSessionRef 148 if err := json.Unmarshal([]byte(refData), &ref); err != nil { 149 return nil, err 150 } 151 152 did, err := syntax.ParseDID(ref.DID) 153 if err != nil { 154 return nil, err 155 } 156 157 return s.GetSession(ctx, did, ref.SessionID) 158} 159 160// SetCurrentSession sets the current active session reference 161func (s *KeyringAuthStore) SetCurrentSession(ctx context.Context, sess *oauth.ClientSessionData) error { 162 slog.Debug("SetCurrentSession called", "did", sess.AccountDID, "sessionID", sess.SessionID) 163 ref := CurrentSessionRef{ 164 DID: sess.AccountDID.String(), 165 SessionID: sess.SessionID, 166 } 167 168 data, err := json.Marshal(ref) 169 if err != nil { 170 slog.Error("SetCurrentSession marshal failed", "err", err) 171 return err 172 } 173 174 slog.Debug("SetCurrentSession saving", "data", string(data)) 175 err = s.keyring.Set(keyringService, currentSessionKey, string(data)) 176 if err != nil { 177 slog.Error("SetCurrentSession keyring.Set failed", "err", err) 178 } else { 179 slog.Debug("SetCurrentSession success") 180 } 181 return err 182} 183 184// ClearCurrentSession removes the current session reference 185func (s *KeyringAuthStore) ClearCurrentSession() error { 186 return s.keyring.Delete(keyringService, currentSessionKey) 187} 188 189// GetLoginIdentifier retrieves the stored login identifier (handle or PDS URL) 190func (s *KeyringAuthStore) GetLoginIdentifier() (string, error) { 191 return s.keyring.Get(keyringService, loginIdentifierKey) 192} 193 194// SetLoginIdentifier stores the login identifier for re-authentication 195func (s *KeyringAuthStore) SetLoginIdentifier(id string) error { 196 return s.keyring.Set(keyringService, loginIdentifierKey, id) 197} 198 199// ClearLoginIdentifier removes the stored login identifier 200func (s *KeyringAuthStore) ClearLoginIdentifier() error { 201 return s.keyring.Delete(keyringService, loginIdentifierKey) 202}