package identity import ( "context" "encoding/json" "errors" "io" "net/http" "net/url" "time" ) var ErrInvalidCode = errors.New("WeChat login code is invalid or expired") var errUnavailable = errors.New("WeChat identity verification is unavailable") // WeChat exchanges wx.login codes at the server only. Client is an optional // network dependency for tests; production uses a bounded HTTPS client. type WeChat struct { AppID string AppSecret string Client *http.Client } // Exchange returns an app-scoped openid after WeChat verifies the code. It never // returns session_key or wraps URL errors, since exchange URLs contain secrets. func (w *WeChat) Exchange(ctx context.Context, code string) (string, error) { if w.AppID == "" || w.AppSecret == "" { return "", errUnavailable } query := url.Values{"appid": {w.AppID}, "secret": {w.AppSecret}, "js_code": {code}, "grant_type": {"authorization_code"}} request, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.weixin.qq.com/sns/jscode2session?"+query.Encode(), nil) if err != nil { return "", errUnavailable } client := w.Client if client == nil { client = &http.Client{Timeout: 5 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} } response, err := client.Do(request) if err != nil { return "", errUnavailable } defer response.Body.Close() if response.StatusCode != 200 { return "", errUnavailable } var result struct { OpenID string `json:"openid"` ErrCode int `json:"errcode"` } if err = json.NewDecoder(io.LimitReader(response.Body, 16*1024)).Decode(&result); err != nil { return "", errUnavailable } if result.ErrCode == 40029 || result.ErrCode == 40163 { return "", ErrInvalidCode } if result.ErrCode != 0 || result.OpenID == "" { return "", errUnavailable } return w.AppID + ":" + result.OpenID, nil }