Files
user-system/internal/auth/providers/github.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
}