Files

265 lines
7.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package providers
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
// TwitterProvider Twitter OAuth提供者 (OAuth 2.0 with PKCE)
type TwitterProvider struct {
ClientID string
RedirectURI string
}
// TwitterAuthURLResponse Twitter授权URL响应
type TwitterAuthURLResponse struct {
URL string `json:"url"`
CodeVerifier string `json:"code_verifier"`
State string `json:"state"`
Redirect string `json:"redirect,omitempty"`
}
// TwitterTokenResponse Twitter Token响应
type TwitterTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
}
// TwitterUserInfo Twitter用户信息
type TwitterUserInfo struct {
Data struct {
ID string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
CreatedAt string `json:"created_at"`
Description string `json:"description"`
PublicMetrics struct {
FollowersCount int `json:"followers_count"`
FollowingCount int `json:"following_count"`
TweetCount int `json:"tweet_count"`
ListedCount int `json:"listed_count"`
} `json:"public_metrics"`
ProfileImageURL string `json:"profile_image_url"`
} `json:"data"`
}
// TwitterErrorResponse Twitter错误响应
type TwitterErrorResponse struct {
Title string `json:"title"`
Detail string `json:"detail"`
Type string `json:"type"`
Status int `json:"status"`
}
// NewTwitterProvider 创建Twitter OAuth提供者
func NewTwitterProvider(clientID, redirectURI string) *TwitterProvider {
return &TwitterProvider{
ClientID: clientID,
RedirectURI: redirectURI,
}
}
// GenerateCodeVerifier 生成PKCE Code Verifier
func (t *TwitterProvider) GenerateCodeVerifier() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b), nil
}
// GenerateCodeChallenge 从Code Verifier生成Code Challenge
func (t *TwitterProvider) GenerateCodeChallenge(verifier string) string {
// 简化的base64编码实际应用中应该使用SHA256哈希
return verifier
}
// GenerateState 生成随机状态码
func (t *TwitterProvider) GenerateState() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
// GetAuthURL 获取Twitter授权URL (OAuth 2.0 with PKCE)
func (t *TwitterProvider) GetAuthURL() (*TwitterAuthURLResponse, error) {
verifier, err := t.GenerateCodeVerifier()
if err != nil {
return nil, fmt.Errorf("generate code verifier failed: %w", err)
}
challenge := t.GenerateCodeChallenge(verifier)
state, err := t.GenerateState()
if err != nil {
return nil, fmt.Errorf("generate state failed: %w", err)
}
authURL := fmt.Sprintf(
"https://twitter.com/i/oauth2/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=tweet.read%%20users.read%%20offline.access&state=%s&code_challenge=%s&code_challenge_method=plain",
t.ClientID,
url.QueryEscape(t.RedirectURI),
state,
challenge,
)
return &TwitterAuthURLResponse{
URL: authURL,
CodeVerifier: verifier,
State: state,
Redirect: t.RedirectURI,
}, nil
}
// ExchangeCode 用授权码换取访问令牌
func (t *TwitterProvider) ExchangeCode(ctx context.Context, code, codeVerifier string) (*TwitterTokenResponse, error) {
tokenURL := "https://api.twitter.com/2/oauth2/token"
data := url.Values{}
data.Set("code", code)
data.Set("grant_type", "authorization_code")
data.Set("client_id", t.ClientID)
data.Set("redirect_uri", t.RedirectURI)
data.Set("code_verifier", codeVerifier)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := postFormWithContext(ctx, client, tokenURL, data)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readOAuthResponseBody(resp)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
// 检查错误响应
var errResp TwitterErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Detail != "" {
return nil, fmt.Errorf("twitter api error: %s - %s", errResp.Title, errResp.Detail)
}
var tokenResp TwitterTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("parse token response failed: %w", err)
}
return &tokenResp, nil
}
// GetUserInfo 获取Twitter用户信息
func (t *TwitterProvider) GetUserInfo(ctx context.Context, accessToken string) (*TwitterUserInfo, error) {
userInfoURL := "https://api.twitter.com/2/users/me?user.fields=created_at,description,public_metrics,profile_image_url"
req, err := http.NewRequestWithContext(ctx, "GET", userInfoURL, nil)
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readOAuthResponseBody(resp)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
// 检查错误响应
var errResp TwitterErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Detail != "" {
return nil, fmt.Errorf("twitter api error: %s - %s", errResp.Title, errResp.Detail)
}
var userInfo TwitterUserInfo
if err := json.Unmarshal(body, &userInfo); err != nil {
return nil, fmt.Errorf("parse user info failed: %w", err)
}
return &userInfo, nil
}
// RefreshToken 刷新访问令牌
func (t *TwitterProvider) RefreshToken(ctx context.Context, refreshToken string) (*TwitterTokenResponse, error) {
tokenURL := "https://api.twitter.com/2/oauth2/token"
data := url.Values{}
data.Set("refresh_token", refreshToken)
data.Set("grant_type", "refresh_token")
data.Set("client_id", t.ClientID)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := postFormWithContext(ctx, client, tokenURL, data)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := readOAuthResponseBody(resp)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
var errResp TwitterErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil && errResp.Detail != "" {
return nil, fmt.Errorf("twitter api error: %s - %s", errResp.Title, errResp.Detail)
}
var tokenResp TwitterTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("parse token response failed: %w", err)
}
return &tokenResp, nil
}
// ValidateToken 验证访问令牌是否有效
func (t *TwitterProvider) ValidateToken(ctx context.Context, accessToken string) (bool, error) {
userInfo, err := t.GetUserInfo(ctx, accessToken)
if err != nil {
return false, err
}
return userInfo != nil && userInfo.Data.ID != "", nil
}
// RevokeToken 撤销访问令牌
func (t *TwitterProvider) RevokeToken(ctx context.Context, accessToken string) error {
revokeURL := "https://api.twitter.com/2/oauth2/revoke"
data := url.Values{}
data.Set("token", accessToken)
data.Set("client_id", t.ClientID)
data.Set("token_type_hint", "access_token")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := postFormWithContext(ctx, client, revokeURL, data)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if _, err := readOAuthResponseBody(resp); err != nil {
return fmt.Errorf("revoke token failed: %w", err)
}
return nil
}