···1313 "github.com/bluesky-social/indigo/atproto/syntax"
1414)
15151616+type RefreshCallback = func(ctx context.Context, data PasswordSessionData)
1717+1618type PasswordAuth struct {
1719 Session PasswordSessionData
1818- // TODO: RefreshCallback
19202020- // lock which protects concurrent access to AccessToken and RefreshToken in session data
2121+ // Optional callback function which gets called with updated session data whenever a successful token refresh happens.
2222+ //
2323+ // Note that this function is called while a lock is being held on the overall client, and with a context usually tied to a regular API request call. The callback should either return quickly, or spawn a goroutine. Because of the lock, this callback will never be called concurrently for a single client, but may be called currently across clients.
2424+ RefreshCallback RefreshCallback
2525+2626+ // Lock which protects concurrent access to AccessToken and RefreshToken in session data. Note that this only applies to this particular instance of PasswordAuth.
2127 lk sync.RWMutex
2228}
2329···139145140146 a.Session.AccessToken = out.AccessJwt
141147 a.Session.RefreshToken = out.RefreshJwt
142142- // TODO: callback?
148148+149149+ if a.RefreshCallback != nil {
150150+ snapshot := a.Session.Clone()
151151+ a.RefreshCallback(ctx, snapshot)
152152+ }
143153144154 return nil
145155}
···174184 return nil
175185}
176186177177-func LoginWithPassword(ctx context.Context, dir identity.Directory, username syntax.AtIdentifier, password, authToken string) (*APIClient, error) {
187187+// Creates a new APIClient with PasswordAuth for the provided user. The provided identity directory is used to resolve the PDS host for the account.
188188+//
189189+// `authToken` is optional; is used when multi-factor authentication is enabled for the account.
190190+// `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine.
191191+func LoginWithPassword(ctx context.Context, dir identity.Directory, username syntax.AtIdentifier, password, authToken string, cb RefreshCallback) (*APIClient, error) {
178192179193 ident, err := dir.Lookup(ctx, username)
180194 if err != nil {
···186200 return nil, fmt.Errorf("account does not have PDS registered")
187201 }
188202203203+ c, err := LoginWithPasswordHost(ctx, host, ident.DID.String(), password, authToken, cb)
204204+ if err != nil {
205205+ return nil, err
206206+ }
207207+208208+ if c.AccountDID == nil || *c.AccountDID != ident.DID {
209209+ return nil, fmt.Errorf("returned session DID not requested account: %s", c.AccountDID)
210210+ }
211211+212212+ return c, nil
213213+}
214214+215215+// Creates a new APIClient with PasswordAuth, based on a login to the provided host. Note that with some PDS implementations, 'username' could be an email address. This login method also works in situations where an account's network identity does not resolve to this specific host.
216216+//
217217+// `authToken` is optional; is used when multi-factor authentication is enabled for the account.
218218+// `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine.
219219+func LoginWithPasswordHost(ctx context.Context, host, username, password, authToken string, cb RefreshCallback) (*APIClient, error) {
220220+189221 c := NewAPIClient(host)
190222 reqBody := comatproto.ServerCreateSession_Input{
191191- Identifier: ident.DID.String(),
223223+ Identifier: username,
192224 Password: password,
193225 }
194226 if authToken != "" {
···205237 return nil, fmt.Errorf("account is disabled: %v", out.Status)
206238 }
207239208208- if out.Did != ident.DID.String() {
209209- return nil, fmt.Errorf("returned session DID not requested account: %s", out.Did)
240240+ did, err := syntax.ParseDID(out.Did)
241241+ if err != nil {
242242+ return nil, err
210243 }
211244212245 ra := PasswordAuth{
213246 Session: PasswordSessionData{
214247 AccessToken: out.AccessJwt,
215248 RefreshToken: out.RefreshJwt,
216216- AccountDID: ident.DID,
249249+ AccountDID: did,
217250 Host: c.Host,
218251 },
252252+ RefreshCallback: cb,
219253 }
220254 c.Auth = &ra
221221- c.AccountDID = &ident.DID
255255+ c.AccountDID = &did
222256 return c, nil
223257}
224258225225-func ResumePasswordSession(data PasswordSessionData) *APIClient {
259259+// Creates an APIClient using PasswordAuth, based on existing session data.
260260+//
261261+// `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine.
262262+func ResumePasswordSession(data PasswordSessionData, cb RefreshCallback) *APIClient {
226263 c := NewAPIClient(data.Host)
227264 ra := PasswordAuth{
228228- Session: data,
265265+ Session: data,
266266+ RefreshCallback: cb,
229267 }
230268 c.Auth = &ra
231269 c.AccountDID = &data.AccountDID