265 lines
7.6 KiB
Go
265 lines
7.6 KiB
Go
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
|
||
}
|