A community based topic aggregation platform built on atproto
at main 101 lines 3.4 kB view raw
1package communities 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 9 "github.com/bluesky-social/indigo/api/atproto" 10 "github.com/bluesky-social/indigo/xrpc" 11) 12 13// refreshPDSToken exchanges a refresh token for new access and refresh tokens 14// Uses com.atproto.server.refreshSession endpoint via Indigo SDK 15// CRITICAL: Refresh tokens are single-use - old refresh token is revoked on success 16func refreshPDSToken(ctx context.Context, pdsURL, refreshToken string) (newAccessToken, newRefreshToken string, err error) { 17 if pdsURL == "" { 18 return "", "", fmt.Errorf("PDS URL is required") 19 } 20 if refreshToken == "" { 21 return "", "", fmt.Errorf("refresh token is required") 22 } 23 24 // Create XRPC client with refresh token as the auth credential 25 // IMPORTANT: The xrpc client always sends AccessJwt as the Authorization header, 26 // but refreshSession requires the refresh token in that header. 27 // So we put the refresh token in AccessJwt to make it work correctly. 28 client := &xrpc.Client{ 29 Host: pdsURL, 30 Auth: &xrpc.AuthInfo{ 31 AccessJwt: refreshToken, // Refresh token goes here (sent as Authorization header) 32 RefreshJwt: refreshToken, // Also set here for completeness 33 }, 34 } 35 36 // Call com.atproto.server.refreshSession 37 output, err := atproto.ServerRefreshSession(ctx, client) 38 if err != nil { 39 // Check for expired refresh token (401 Unauthorized) 40 // Try typed error first (more reliable), fallback to string check 41 var xrpcErr *xrpc.Error 42 if errors.As(err, &xrpcErr) && xrpcErr.StatusCode == 401 { 43 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)") 44 } 45 46 // Fallback: string-based detection (in case error isn't wrapped as xrpc.Error) 47 errStr := err.Error() 48 if strings.Contains(errStr, "401") || strings.Contains(errStr, "Unauthorized") { 49 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)") 50 } 51 52 return "", "", fmt.Errorf("failed to refresh session: %w", err) 53 } 54 55 // Validate response 56 if output.AccessJwt == "" || output.RefreshJwt == "" { 57 return "", "", fmt.Errorf("refresh response missing tokens") 58 } 59 60 return output.AccessJwt, output.RefreshJwt, nil 61} 62 63// reauthenticateWithPassword creates a new session using stored credentials 64// This is the fallback when refresh tokens expire (after ~2 months) 65// Uses com.atproto.server.createSession endpoint via Indigo SDK 66func reauthenticateWithPassword(ctx context.Context, pdsURL, email, password string) (accessToken, refreshToken string, err error) { 67 if pdsURL == "" { 68 return "", "", fmt.Errorf("PDS URL is required") 69 } 70 if email == "" { 71 return "", "", fmt.Errorf("email is required") 72 } 73 if password == "" { 74 return "", "", fmt.Errorf("password is required") 75 } 76 77 // Create unauthenticated XRPC client 78 client := &xrpc.Client{ 79 Host: pdsURL, 80 } 81 82 // Prepare createSession input 83 // The identifier can be either email or handle 84 input := &atproto.ServerCreateSession_Input{ 85 Identifier: email, 86 Password: password, 87 } 88 89 // Call com.atproto.server.createSession 90 output, err := atproto.ServerCreateSession(ctx, client, input) 91 if err != nil { 92 return "", "", fmt.Errorf("failed to create session: %w", err) 93 } 94 95 // Validate response 96 if output.AccessJwt == "" || output.RefreshJwt == "" { 97 return "", "", fmt.Errorf("createSession response missing tokens") 98 } 99 100 return output.AccessJwt, output.RefreshJwt, nil 101}