wechatverify
github.com/gonewx/wicket/wechatverify uses an authorization code on the backend to obtain a user identifier from WeChat. After the app completes authorization through the WeChat SDK, it sends the code to your backend; the backend calls this package and gets a unionid that's stable across apps (falling back to the openid when no unionid is available).
Integration testing status
Real WeChat Open Platform credentials aren't available yet, so integration testing against the real service hasn't been done. Current verification covers: request/response matching against a stub service, error classification, secret redaction, and the "unavailable when credentials aren't configured" semantics. Before going live, run an end-to-end check with your own credentials.
Basics
| Item | Value |
|---|---|
| API called | GET https://api.weixin.qq.com/sns/oauth2/access_token (constant DefaultEndpoint) |
| Applicable product | WeChat login for mobile apps. Mini Program login is a different product with a different response format, and isn't supported by this package |
| Dependencies | Go standard library only |
| Entry points | NewVerifier(Config) (Verifier, error); Verifier.Verify(ctx, code) (*VerifiedIdentity, error) |
Example
The following code has been compiled and run against a local stub service (stub omitted):
import (
"context"
"errors"
"net/http"
"os"
"github.com/gonewx/wicket/wechatverify"
)
func newWeChat() (wechatverify.Verifier, error) {
return wechatverify.NewVerifier(wechatverify.Config{
AppID: os.Getenv("WECHAT_APP_ID"),
AppSecret: os.Getenv("WECHAT_APP_SECRET"),
})
}
func handleWeChatLogin(v wechatverify.Verifier, w http.ResponseWriter, r *http.Request) {
id, err := v.Verify(r.Context(), r.FormValue("code"))
switch {
case err == nil:
if id.SubjectSource == wechatverify.SubjectFromOpenID {
// Degraded: only an app-scoped openid was obtained; decide per your business policy whether to accept it
}
_ = id.Subject // use as the external account key to find or create a local user
w.WriteHeader(http.StatusOK)
case errors.Is(err, wechatverify.ErrInvalidCredential):
http.Error(w, "invalid code", http.StatusUnauthorized)
case errors.Is(err, wechatverify.ErrRetryable), errors.Is(err, wechatverify.ErrUnavailable):
// Both mean "can't decide right now"; ErrUnavailable needs an alert
http.Error(w, "try again later", http.StatusServiceUnavailable)
case errors.Is(err, context.Canceled):
// The client has given up
default: // ErrMalformedResponse
http.Error(w, "upstream error", http.StatusBadGateway)
}
}NewVerifier returns ErrUnavailable when AppID or AppSecret is missing. Construct it at startup: when credentials are missing, the whole capability stays off (returning 503 externally), instead of failing when the first user signs in.
Configuration: Config
| Field | Type | Description |
|---|---|---|
AppID | string | The mobile app's AppID; required |
AppSecret | string | The app secret; required |
Endpoint | string | Overrides the API URL (test stub, proxy); empty uses DefaultEndpoint. Must be http/https, have a host name, and carry no query string; otherwise ErrUnavailable |
HTTPClient | *http.Client | Custom client; when empty, a built-in client is used |
Timeout | time.Duration | Timeout for the built-in client; 10 seconds when ≤ 0; ignored when HTTPClient is provided |
The built-in client doesn't follow redirects: a 3xx from the API is treated as an abnormal response (ErrMalformedResponse). If you bring your own HTTPClient, also set CheckRedirect to return http.ErrUseLastResponse, so requests carrying the secret aren't forwarded elsewhere.
Identity projection and degradation marker
type VerifiedIdentity struct {
Subject string // unionid preferred; openid when missing
SubjectSource SubjectSource // SubjectFromUnionID ("unionid") or SubjectFromOpenID ("openid")
UnionID string // raw value, may be empty
OpenID string // raw value, always non-empty on success
Scope string // the scope the user authorized, passed through as-is
}- A unionid is returned only when the app is bound to a WeChat Open Platform account and WeChat's conditions for issuing it are met. A missing unionid is normal, not an error.
- In that case
Subjectfalls back to the openid, andSubjectSourceisSubjectFromOpenID. This is a degradation marker: an openid is stable only within the current app, and the same person has a different openid in another of your apps. If your account system spans multiple apps, use this to reject the login or guide the user through account binding, rather than creating an account directly. - A successful response must carry an openid; if the API claims success without an openid, it's rejected as
ErrMalformedResponse, and no identity is issued even if a unionid is present. - The WeChat
access_tokenobtained from the exchange is parsed but neither used nor exposed; success depends only on whethererrcodeis 0 and whether an openid is present.
Error categories
Check the four sentinels with errors.Is; when you need WeChat's raw return code, use errors.As to get *VendorError (fields Code int, Msg string).
| Sentinel | Triggered by |
|---|---|
ErrInvalidCredential | Empty code (rejected without sending a request); return codes 40029 (invalid code), 40163 (code already used), 41008 (missing code), 40013 (invalid AppID), 40125 (invalid secret), 40001 (credential mismatch); and any unrecognized non-zero return code |
ErrRetryable | Return codes -1 (system busy), 45011 (rate limit exceeded); HTTP 5xx, 429, 408; network transport failure; failure reading the response body; ctx deadline expired |
ErrMalformedResponse | Any other non-2xx status (including 3xx); a response body that isn't valid JSON; return code 0 but no openid |
ErrUnavailable | AppID / AppSecret not configured; invalid Endpoint; the request can't be constructed |
Notes:
- WeChat uses HTTP 200 for both success and failure, and only a non-zero
errcodeindicates failure. This package handles that; callers only need to look at the error. - App-side configuration errors (40013, 40125, 40001) are also classified as
ErrInvalidCredentialin this package, with no distinction from an invalid user code. If you need to tell them apart for alerting, useerrors.Asto getVendorError.Codeand decide yourself. - When
ctxis canceled, the error satisfieserrors.Is(err, context.Canceled)and does not satisfyErrRetryable; whenctx's deadline expires, it satisfies bothcontext.DeadlineExceededandErrRetryable. - At most 1 MiB of the response body is read, and response body excerpts quoted in errors are at most 256 bytes.
Keeping the secret from leaking
The secret is sent with the request as a query parameter, so on a network failure the error Go returns contains the full URL. This package scrubs all text that can reach the caller:
- The secret in error messages is replaced with
[REDACTED]; both the raw and URL-encoded forms are matched. Observed error on a transport failure:... exchange request failed: Get "http://127.0.0.1:1?appid=wx&code=c&grant_type=authorization_code&secret=[REDACTED]": ... connection refused. VendorError.Msgis scrubbed as well.ConfigimplementsString(),MarshalJSON(), andslog.LogValuer: infmt's%v/%+v/%s,json.Marshal, andslogoutput, the secret is[REDACTED]. Deserialization is unaffected; configuration can still be read from JSON.- The verifier instance's
%voutput is scrubbed too.
Two gaps outside this protection
- In v0.1.4 the
%#vformat isn't covered by the redaction above. Don't print or log the configuration with%#v. - If you pull out the
cfg.AppSecretfield yourself and print it, the package can't protect it.
The package itself doesn't write any logs.
Relationship to the container
The container registers an empty *wechatverify.Config singleton (root.Ptr_Config()) as a configuration extension point, but doesn't register a Verifier—constructing one from an empty configuration always fails. The simplest approach is to construct the verifier directly in your initialization code.
If you choose to write credentials into this shared pointer in the container, write it only during wiring, before you start handling requests: it's shared by all scopes, so writing while requests are in flight races with readers; and if you clear a field at that point, the next construction silently becomes ErrUnavailable.