Skip to content

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 ​

ItemValue
API calledGET https://api.weixin.qq.com/sns/oauth2/access_token (constant DefaultEndpoint)
Applicable productWeChat login for mobile apps. Mini Program login is a different product with a different response format, and isn't supported by this package
DependenciesGo standard library only
Entry pointsNewVerifier(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):

go
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 ​

FieldTypeDescription
AppIDstringThe mobile app's AppID; required
AppSecretstringThe app secret; required
EndpointstringOverrides 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.ClientCustom client; when empty, a built-in client is used
Timeouttime.DurationTimeout 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 ​

go
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 Subject falls back to the openid, and SubjectSource is SubjectFromOpenID. 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_token obtained from the exchange is parsed but neither used nor exposed; success depends only on whether errcode is 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).

SentinelTriggered by
ErrInvalidCredentialEmpty 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
ErrRetryableReturn codes -1 (system busy), 45011 (rate limit exceeded); HTTP 5xx, 429, 408; network transport failure; failure reading the response body; ctx deadline expired
ErrMalformedResponseAny other non-2xx status (including 3xx); a response body that isn't valid JSON; return code 0 but no openid
ErrUnavailableAppID / 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 errcode indicates failure. This package handles that; callers only need to look at the error.
  • App-side configuration errors (40013, 40125, 40001) are also classified as ErrInvalidCredential in this package, with no distinction from an invalid user code. If you need to tell them apart for alerting, use errors.As to get VendorError.Code and decide yourself.
  • When ctx is canceled, the error satisfies errors.Is(err, context.Canceled) and does not satisfy ErrRetryable; when ctx's deadline expires, it satisfies both context.DeadlineExceeded and ErrRetryable.
  • 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.Msg is scrubbed as well.
  • Config implements String(), MarshalJSON(), and slog.LogValuer: in fmt's %v / %+v / %s, json.Marshal, and slog output, the secret is [REDACTED]. Deserialization is unaffected; configuration can still be read from JSON.
  • The verifier instance's %v output is scrubbed too.

Two gaps outside this protection

  • In v0.1.4 the %#v format isn't covered by the redaction above. Don't print or log the configuration with %#v.
  • If you pull out the cfg.AppSecret field 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.

Released under the Apache License 2.0 · Documentation © 2026 Wicket authors