Monorepo for Tangled tangled.org

appview/oauth: implement apppassword session refreshing

do not refresh after token expiry, do it before

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li b19d8ca9 35ec6890

verified
+61 -12
+61 -12
appview/oauth/handler.go
··· 145 145 } 146 146 147 147 if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { 148 - o.invalidateAppPasswordSession() 149 148 l.Error("failed to add to default spindle", "err", err) 150 149 return 151 150 } ··· 185 184 } 186 185 187 186 if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { 188 - o.invalidateAppPasswordSession() 189 187 l.Error("failed to add to default knot", "err", err) 190 188 return 191 189 } ··· 195 193 return 196 194 } 197 195 198 - l.Debug("successfully addeds to default Knot") 196 + l.Debug("successfully added to default knot") 199 197 } 200 198 201 199 func (o *OAuth) ensureTangledProfile(sessData *oauth.ClientSessionData) { ··· 248 246 // create a AppPasswordSession using apppasswords 249 247 type AppPasswordSession struct { 250 248 AccessJwt string `json:"accessJwt"` 249 + RefreshJwt string `json:"refreshJwt"` 251 250 PdsEndpoint string 252 251 Did string 253 252 Logger *slog.Logger 253 + ExpiresAt time.Time 254 254 } 255 255 256 256 func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did string, logger *slog.Logger) (*AppPasswordSession, error) { ··· 305 305 session.PdsEndpoint = pdsEndpoint 306 306 session.Did = did 307 307 session.Logger = logger 308 + session.ExpiresAt = time.Now().Add(115 * time.Minute) 308 309 309 310 return &session, nil 310 311 } 311 312 313 + func (s *AppPasswordSession) refreshSession() error { 314 + refreshURL := s.PdsEndpoint + "/xrpc/com.atproto.server.refreshSession" 315 + req, err := http.NewRequestWithContext(context.Background(), "POST", refreshURL, nil) 316 + if err != nil { 317 + return fmt.Errorf("failed to create refresh request: %w", err) 318 + } 319 + 320 + req.Header.Set("Authorization", "Bearer "+s.RefreshJwt) 321 + 322 + s.Logger.Debug("refreshing app password session", "url", refreshURL) 323 + 324 + client := &http.Client{Timeout: 30 * time.Second} 325 + resp, err := client.Do(req) 326 + if err != nil { 327 + return fmt.Errorf("failed to refresh session: %w", err) 328 + } 329 + defer resp.Body.Close() 330 + 331 + if resp.StatusCode != http.StatusOK { 332 + var errorResponse map[string]any 333 + if err := json.NewDecoder(resp.Body).Decode(&errorResponse); err != nil { 334 + return fmt.Errorf("failed to refresh session: HTTP %d (failed to decode error response: %w)", resp.StatusCode, err) 335 + } 336 + errorBytes, _ := json.Marshal(errorResponse) 337 + return fmt.Errorf("failed to refresh session: HTTP %d, response: %s", resp.StatusCode, string(errorBytes)) 338 + } 339 + 340 + var refreshResponse struct { 341 + AccessJwt string `json:"accessJwt"` 342 + RefreshJwt string `json:"refreshJwt"` 343 + } 344 + if err := json.NewDecoder(resp.Body).Decode(&refreshResponse); err != nil { 345 + return fmt.Errorf("failed to decode refresh response: %w", err) 346 + } 347 + 348 + s.AccessJwt = refreshResponse.AccessJwt 349 + s.RefreshJwt = refreshResponse.RefreshJwt 350 + // Set new expiry time with 5 minute buffer 351 + s.ExpiresAt = time.Now().Add(115 * time.Minute) 352 + 353 + s.Logger.Debug("successfully refreshed app password session") 354 + return nil 355 + } 356 + 357 + func (s *AppPasswordSession) isValid() bool { 358 + return time.Now().Before(s.ExpiresAt) 359 + } 360 + 312 361 func (s *AppPasswordSession) putRecord(record any, collection string) error { 362 + if !s.isValid() { 363 + s.Logger.Debug("access token expired, refreshing session") 364 + if err := s.refreshSession(); err != nil { 365 + return fmt.Errorf("failed to refresh session: %w", err) 366 + } 367 + s.Logger.Debug("session refreshed") 368 + } 369 + 313 370 recordBytes, err := json.Marshal(record) 314 371 if err != nil { 315 372 return fmt.Errorf("failed to marshal knot member record: %w", err) ··· 336 393 req.Header.Set("Content-Type", "application/json") 337 394 req.Header.Set("Authorization", "Bearer "+s.AccessJwt) 338 395 339 - s.Logger.Debug("putting record", "url", url, "collection", collection, "headers", req.Header) 396 + s.Logger.Debug("putting record", "url", url, "collection", collection) 340 397 341 398 client := &http.Client{Timeout: 30 * time.Second} 342 399 resp, err := client.Do(req) ··· 373 430 o.appPasswordSession = session 374 431 return session, nil 375 432 } 376 - 377 - // invalidateAppPasswordSession clears the cached session so the next call to 378 - // getAppPasswordSession will create a fresh one. 379 - func (o *OAuth) invalidateAppPasswordSession() { 380 - o.appPasswordSessionMu.Lock() 381 - defer o.appPasswordSessionMu.Unlock() 382 - o.appPasswordSession = nil 383 - }