173 lines
4.6 KiB
Go
173 lines
4.6 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// GitHubProvider GitHub OAuth提供者
|
|
type GitHubProvider struct {
|
|
ClientID string
|
|
ClientSecret string
|
|
RedirectURI string
|
|
}
|
|
|
|
// GitHubTokenResponse GitHub Token响应
|
|
type GitHubTokenResponse struct {
|
|
AccessToken string `json:"access_token"`
|
|
TokenType string `json:"token_type"`
|
|
Scope string `json:"scope"`
|
|
}
|
|
|
|
// GitHubUserInfo GitHub用户信息
|
|
type GitHubUserInfo struct {
|
|
ID int64 `json:"id"`
|
|
Login string `json:"login"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
AvatarURL string `json:"avatar_url"`
|
|
Bio string `json:"bio"`
|
|
Location string `json:"location"`
|
|
}
|
|
|
|
// NewGitHubProvider 创建GitHub OAuth提供者
|
|
func NewGitHubProvider(clientID, clientSecret, redirectURI string) *GitHubProvider {
|
|
return &GitHubProvider{
|
|
ClientID: clientID,
|
|
ClientSecret: clientSecret,
|
|
RedirectURI: redirectURI,
|
|
}
|
|
}
|
|
|
|
// GetAuthURL 获取GitHub授权URL
|
|
func (g *GitHubProvider) GetAuthURL(state string) (string, error) {
|
|
authURL := fmt.Sprintf(
|
|
"https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=read:user,user:email&state=%s",
|
|
g.ClientID,
|
|
url.QueryEscape(g.RedirectURI),
|
|
url.QueryEscape(state),
|
|
)
|
|
return authURL, nil
|
|
}
|
|
|
|
// ExchangeCode 用授权码换取访问令牌
|
|
func (g *GitHubProvider) ExchangeCode(ctx context.Context, code string) (*GitHubTokenResponse, error) {
|
|
tokenURL := "https://github.com/login/oauth/access_token"
|
|
|
|
data := url.Values{}
|
|
data.Set("client_id", g.ClientID)
|
|
data.Set("client_secret", g.ClientSecret)
|
|
data.Set("code", code)
|
|
data.Set("redirect_uri", g.RedirectURI)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL,
|
|
strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request failed: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
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 tokenResp GitHubTokenResponse
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
|
return nil, fmt.Errorf("parse token response failed: %w", err)
|
|
}
|
|
|
|
if tokenResp.AccessToken == "" {
|
|
return nil, fmt.Errorf("GitHub OAuth: empty access token in response")
|
|
}
|
|
|
|
return &tokenResp, nil
|
|
}
|
|
|
|
// GetUserInfo 获取GitHub用户信息
|
|
func (g *GitHubProvider) GetUserInfo(ctx context.Context, accessToken string) (*GitHubUserInfo, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request failed: %w", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
|
|
|
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 userInfo GitHubUserInfo
|
|
if err := json.Unmarshal(body, &userInfo); err != nil {
|
|
return nil, fmt.Errorf("parse user info failed: %w", err)
|
|
}
|
|
|
|
// 如果用户信息中的邮箱为空,尝试通过邮箱 API 获取主要邮箱
|
|
if userInfo.Email == "" {
|
|
email, _ := g.getPrimaryEmail(ctx, accessToken)
|
|
userInfo.Email = email
|
|
}
|
|
|
|
return &userInfo, nil
|
|
}
|
|
|
|
// getPrimaryEmail 获取用户的主要邮箱
|
|
func (g *GitHubProvider) getPrimaryEmail(ctx context.Context, accessToken string) (string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user/emails", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("Accept", "application/vnd.github+json")
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := readOAuthResponseBody(resp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var emails []struct {
|
|
Email string `json:"email"`
|
|
Primary bool `json:"primary"`
|
|
Verified bool `json:"verified"`
|
|
}
|
|
if err := json.Unmarshal(body, &emails); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for _, e := range emails {
|
|
if e.Primary && e.Verified {
|
|
return e.Email, nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|