Upload images to your PDS and get instant CDN URLs via images.blue
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}