thread-master/apps/backend/internal/module/member/usecase/oauth_providers.go

131 lines
3.7 KiB
Go
Raw Normal View History

2026-07-10 12:54:45 +00:00
package usecase
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
2026-07-15 15:23:59 +00:00
"strconv"
2026-07-10 12:54:45 +00:00
"strings"
"time"
)
2026-07-15 15:23:59 +00:00
type googleIDTokenInfoResp struct {
Audience string `json:"aud"`
Issuer string `json:"iss"`
Subject string `json:"sub"`
Email string `json:"email"`
Name string `json:"name"`
Expires string `json:"exp"`
2026-07-10 12:54:45 +00:00
}
2026-07-15 15:23:59 +00:00
func googleIDTokenInfo(ctx context.Context, idToken, clientID string) (*googleIDTokenInfoResp, error) {
endpoint := "https://oauth2.googleapis.com/tokeninfo?id_token=" + url.QueryEscape(strings.TrimSpace(idToken))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
2026-07-10 12:54:45 +00:00
if err != nil {
return nil, err
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
2026-07-15 15:23:59 +00:00
return nil, fmt.Errorf("google tokeninfo status %d", resp.StatusCode)
2026-07-10 12:54:45 +00:00
}
2026-07-15 15:23:59 +00:00
var info googleIDTokenInfoResp
2026-07-10 12:54:45 +00:00
if err := json.Unmarshal(body, &info); err != nil {
return nil, err
}
2026-07-15 15:23:59 +00:00
if err := validateGoogleIDTokenInfo(&info, clientID, time.Now()); err != nil {
return nil, err
2026-07-10 12:54:45 +00:00
}
return &info, nil
}
2026-07-15 15:23:59 +00:00
func validateGoogleIDTokenInfo(info *googleIDTokenInfoResp, clientID string, now time.Time) error {
if info == nil || info.Subject == "" || info.Audience != strings.TrimSpace(clientID) {
return fmt.Errorf("google ID token audience or subject is invalid")
}
if info.Issuer != "accounts.google.com" && info.Issuer != "https://accounts.google.com" {
return fmt.Errorf("google ID token issuer is invalid")
}
expires, err := strconv.ParseInt(info.Expires, 10, 64)
if err != nil || expires <= now.Unix() {
return fmt.Errorf("google ID token is expired")
}
return nil
}
2026-07-10 12:54:45 +00:00
type lineTokenResp struct {
AccessToken string `json:"access_token"`
}
type lineProfileResp struct {
UserID string `json:"userId"`
DisplayName string `json:"displayName"`
}
func lineExchangeCode(ctx context.Context, code, redirectURI, clientID, clientSecret string) (string, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("code", code)
data.Set("redirect_uri", redirectURI)
data.Set("client_id", clientID)
data.Set("client_secret", clientSecret)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.line.me/oauth2/v2.1/token",
strings.NewReader(data.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("line token status %d: %s", resp.StatusCode, string(body))
}
var tok lineTokenResp
if err := json.Unmarshal(body, &tok); err != nil {
return "", err
}
if tok.AccessToken == "" {
return "", fmt.Errorf("line empty access_token")
}
return tok.AccessToken, nil
}
func lineProfile(ctx context.Context, accessToken string) (*lineProfileResp, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.line.me/v2/profile", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+accessToken)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("line profile status %d: %s", resp.StatusCode, string(body))
}
var p lineProfileResp
if err := json.Unmarshal(body, &p); err != nil {
return nil, err
}
if p.UserID == "" {
return nil, fmt.Errorf("line profile missing userId")
}
return &p, nil
}