62 lines
1.9 KiB
Go
62 lines
1.9 KiB
Go
|
|
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
|
||
|
|
}
|