this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

password auth callbacks, and split host-specific auth

+69 -21
+2 -2
atproto/client/cmd/atclient/main.go
··· 148 148 149 149 dir := identity.DefaultDirectory() 150 150 151 - c, err := client.LoginWithPassword(ctx, dir, *atid, cctx.String("password"), "") 151 + c, err := client.LoginWithPassword(ctx, dir, *atid, cctx.String("password"), "", nil) 152 152 if err != nil { 153 153 return err 154 154 } ··· 177 177 178 178 dir := identity.DefaultDirectory() 179 179 180 - c, err := client.LoginWithPassword(ctx, dir, *atid, cctx.String("password"), "") 180 + c, err := client.LoginWithPassword(ctx, dir, *atid, cctx.String("password"), "", nil) 181 181 if err != nil { 182 182 return err 183 183 }
+49 -11
atproto/client/password_auth.go
··· 13 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 14 ) 15 15 16 + type RefreshCallback = func(ctx context.Context, data PasswordSessionData) 17 + 16 18 type PasswordAuth struct { 17 19 Session PasswordSessionData 18 - // TODO: RefreshCallback 19 20 20 - // lock which protects concurrent access to AccessToken and RefreshToken in session data 21 + // Optional callback function which gets called with updated session data whenever a successful token refresh happens. 22 + // 23 + // 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. 24 + RefreshCallback RefreshCallback 25 + 26 + // Lock which protects concurrent access to AccessToken and RefreshToken in session data. Note that this only applies to this particular instance of PasswordAuth. 21 27 lk sync.RWMutex 22 28 } 23 29 ··· 139 145 140 146 a.Session.AccessToken = out.AccessJwt 141 147 a.Session.RefreshToken = out.RefreshJwt 142 - // TODO: callback? 148 + 149 + if a.RefreshCallback != nil { 150 + snapshot := a.Session.Clone() 151 + a.RefreshCallback(ctx, snapshot) 152 + } 143 153 144 154 return nil 145 155 } ··· 174 184 return nil 175 185 } 176 186 177 - func LoginWithPassword(ctx context.Context, dir identity.Directory, username syntax.AtIdentifier, password, authToken string) (*APIClient, error) { 187 + // Creates a new APIClient with PasswordAuth for the provided user. The provided identity directory is used to resolve the PDS host for the account. 188 + // 189 + // `authToken` is optional; is used when multi-factor authentication is enabled for the account. 190 + // `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine. 191 + func LoginWithPassword(ctx context.Context, dir identity.Directory, username syntax.AtIdentifier, password, authToken string, cb RefreshCallback) (*APIClient, error) { 178 192 179 193 ident, err := dir.Lookup(ctx, username) 180 194 if err != nil { ··· 186 200 return nil, fmt.Errorf("account does not have PDS registered") 187 201 } 188 202 203 + c, err := LoginWithPasswordHost(ctx, host, ident.DID.String(), password, authToken, cb) 204 + if err != nil { 205 + return nil, err 206 + } 207 + 208 + if c.AccountDID == nil || *c.AccountDID != ident.DID { 209 + return nil, fmt.Errorf("returned session DID not requested account: %s", c.AccountDID) 210 + } 211 + 212 + return c, nil 213 + } 214 + 215 + // 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. 216 + // 217 + // `authToken` is optional; is used when multi-factor authentication is enabled for the account. 218 + // `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine. 219 + func LoginWithPasswordHost(ctx context.Context, host, username, password, authToken string, cb RefreshCallback) (*APIClient, error) { 220 + 189 221 c := NewAPIClient(host) 190 222 reqBody := comatproto.ServerCreateSession_Input{ 191 - Identifier: ident.DID.String(), 223 + Identifier: username, 192 224 Password: password, 193 225 } 194 226 if authToken != "" { ··· 205 237 return nil, fmt.Errorf("account is disabled: %v", out.Status) 206 238 } 207 239 208 - if out.Did != ident.DID.String() { 209 - return nil, fmt.Errorf("returned session DID not requested account: %s", out.Did) 240 + did, err := syntax.ParseDID(out.Did) 241 + if err != nil { 242 + return nil, err 210 243 } 211 244 212 245 ra := PasswordAuth{ 213 246 Session: PasswordSessionData{ 214 247 AccessToken: out.AccessJwt, 215 248 RefreshToken: out.RefreshJwt, 216 - AccountDID: ident.DID, 249 + AccountDID: did, 217 250 Host: c.Host, 218 251 }, 252 + RefreshCallback: cb, 219 253 } 220 254 c.Auth = &ra 221 - c.AccountDID = &ident.DID 255 + c.AccountDID = &did 222 256 return c, nil 223 257 } 224 258 225 - func ResumePasswordSession(data PasswordSessionData) *APIClient { 259 + // Creates an APIClient using PasswordAuth, based on existing session data. 260 + // 261 + // `cb` is an optional callback which will be called with updated session data after any token refresh, in a goroutine. 262 + func ResumePasswordSession(data PasswordSessionData, cb RefreshCallback) *APIClient { 226 263 c := NewAPIClient(data.Host) 227 264 ra := PasswordAuth{ 228 - Session: data, 265 + Session: data, 266 + RefreshCallback: cb, 229 267 } 230 268 c.Auth = &ra 231 269 c.AccountDID = &data.AccountDID
+18 -8
atproto/client/password_auth_test.go
··· 113 113 assert := assert.New(t) 114 114 require := require.New(t) 115 115 ctx := context.Background() 116 - //var apierr *APIError 117 116 118 117 srv := httptest.NewServer(http.HandlerFunc(pwHandler)) 119 118 defer srv.Close() ··· 132 131 133 132 { 134 133 // simple GET requests, with token expire/retry 135 - c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "") 134 + c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "", nil) 136 135 require.NoError(err) 137 136 err = c.Get(ctx, syntax.NSID("com.example.get"), nil, nil) 138 137 assert.NoError(err) ··· 141 140 } 142 141 143 142 { 143 + // test resume session, and session data callback mechanism 144 + ch := make(chan string, 10) 145 + cb := func(ctx context.Context, data PasswordSessionData) { 146 + assert.Equal("refresh2", data.RefreshToken) 147 + ch <- "refreshed" 148 + } 144 149 c := ResumePasswordSession(PasswordSessionData{ 145 150 AccessToken: "access1", 146 151 RefreshToken: "refresh1", 147 152 AccountDID: syntax.DID("did:web:account.example.com"), 148 153 Host: srv.URL, 149 - }) 154 + }, cb) 150 155 151 156 err := c.Get(ctx, syntax.NSID("com.example.get"), nil, nil) 152 157 assert.NoError(err) 153 158 err = c.Get(ctx, syntax.NSID("com.example.expire"), nil, nil) 154 159 assert.NoError(err) 160 + 161 + select { 162 + case msg := <-ch: 163 + assert.Equal("refreshed", msg) 164 + } 155 165 } 156 166 157 167 { 158 168 // logout 159 - c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "") 169 + c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "", nil) 160 170 require.NoError(err) 161 171 162 172 passAuth, ok := c.Auth.(*PasswordAuth) ··· 167 177 168 178 { 169 179 // simple POST request, with token expire/retry 170 - c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "") 180 + c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "", nil) 171 181 require.NoError(err) 172 182 body := map[string]any{ 173 183 "a": 123, ··· 182 192 183 193 { 184 194 // POST with bytes.Buffer body 185 - c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "") 195 + c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "", nil) 186 196 require.NoError(err) 187 197 body := bytes.NewBufferString("some text") 188 198 req := NewAPIRequest(MethodProcedure, syntax.NSID("com.example.expire"), body) ··· 194 204 195 205 { 196 206 // POST with file on disk (can seek and retry) 197 - c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "") 207 + c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "", nil) 198 208 require.NoError(err) 199 209 f, err := os.Open("testdata/body.json") 200 210 require.NoError(err) ··· 207 217 208 218 { 209 219 // POST with pipe reader (can *not* retry) 210 - c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "") 220 + c, err := LoginWithPassword(ctx, &dir, syntax.Handle("user1.example.com").AtIdentifier(), "password1", "", nil) 211 221 require.NoError(err) 212 222 r1, w1 := io.Pipe() 213 223 go func() {