package auth import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) type googleTokenInfo struct { Audience string `json:"aud"` Subject string `json:"sub"` Email string `json:"email"` EmailVerified string `json:"email_verified"` Name string `json:"name"` Error string `json:"error"` ErrorDesc string `json:"error_description"` } type wechatTokenResponse struct { AccessToken string `json:"access_token"` OpenID string `json:"openid"` UnionID string `json:"unionid"` Scope string `json:"scope"` ErrCode int64 `json:"errcode"` ErrMsg string `json:"errmsg"` } func (s *Service) verifyGoogleIDToken(ctx context.Context, idToken string) (GoogleIdentity, error) { idToken = strings.TrimSpace(idToken) if idToken == "" { return GoogleIdentity{}, fmt.Errorf("%w: idToken is required", ErrInvalidCredentials) } endpoint := strings.TrimSpace(s.cfg.GoogleTokenInfo) if endpoint == "" || strings.TrimSpace(s.cfg.GoogleClientID) == "" { return GoogleIdentity{}, ErrProviderNotReady } values := url.Values{} values.Set("id_token", idToken) requestURL := endpoint + "?" + values.Encode() timeout := s.cfg.GoogleTimeout if timeout <= 0 { timeout = 8 * time.Second } req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return GoogleIdentity{}, err } client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return GoogleIdentity{}, err } defer resp.Body.Close() var info googleTokenInfo if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { return GoogleIdentity{}, err } if resp.StatusCode >= 400 || info.Error != "" { if info.ErrorDesc != "" { return GoogleIdentity{}, fmt.Errorf("%w: %s", ErrInvalidCredentials, info.ErrorDesc) } return GoogleIdentity{}, ErrInvalidCredentials } if info.Audience != strings.TrimSpace(s.cfg.GoogleClientID) { return GoogleIdentity{}, fmt.Errorf("%w: google audience mismatch", ErrInvalidCredentials) } if info.Subject == "" || info.Email == "" { return GoogleIdentity{}, fmt.Errorf("%w: google identity is incomplete", ErrInvalidCredentials) } if info.EmailVerified != "" && !strings.EqualFold(info.EmailVerified, "true") { return GoogleIdentity{}, fmt.Errorf("%w: google email is not verified", ErrInvalidCredentials) } return GoogleIdentity{ Subject: info.Subject, Email: normalizeEmail(info.Email), Name: strings.TrimSpace(info.Name), }, nil } func (s *Service) exchangeWechatCodeHTTP(ctx context.Context, code string) (WechatIdentity, error) { code = strings.TrimSpace(code) if code == "" { return WechatIdentity{}, fmt.Errorf("%w: code is required", ErrInvalidCredentials) } if strings.TrimSpace(s.cfg.WechatAppID) == "" || strings.TrimSpace(s.cfg.WechatAppSecret) == "" { return WechatIdentity{}, ErrProviderNotReady } endpoint := strings.TrimSpace(s.cfg.WechatTokenURL) if endpoint == "" { return WechatIdentity{}, ErrProviderNotReady } values := url.Values{} values.Set("appid", s.cfg.WechatAppID) values.Set("secret", s.cfg.WechatAppSecret) values.Set("code", code) values.Set("grant_type", "authorization_code") requestURL := endpoint + "?" + values.Encode() timeout := s.cfg.WechatTimeout if timeout <= 0 { timeout = 8 * time.Second } req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return WechatIdentity{}, err } client := &http.Client{Timeout: timeout} resp, err := client.Do(req) if err != nil { return WechatIdentity{}, err } defer resp.Body.Close() var token wechatTokenResponse if err := json.NewDecoder(resp.Body).Decode(&token); err != nil { return WechatIdentity{}, err } if resp.StatusCode >= 400 || token.ErrCode != 0 { if token.ErrMsg != "" { return WechatIdentity{}, fmt.Errorf("%w: %s", ErrInvalidCredentials, token.ErrMsg) } return WechatIdentity{}, ErrInvalidCredentials } if token.OpenID == "" { return WechatIdentity{}, fmt.Errorf("%w: wechat identity is incomplete", ErrInvalidCredentials) } return WechatIdentity{ OpenID: strings.TrimSpace(token.OpenID), UnionID: strings.TrimSpace(token.UnionID), }, nil }