package usecase import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) 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"` } 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) 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 { return nil, fmt.Errorf("google tokeninfo status %d", resp.StatusCode) } var info googleIDTokenInfoResp if err := json.Unmarshal(body, &info); err != nil { return nil, err } if err := validateGoogleIDTokenInfo(&info, clientID, time.Now()); err != nil { return nil, err } return &info, nil } 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 } 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 }