Skip to content

phoneverify ​

github.com/gonewx/wicket/phoneverify uses a number verification token on the backend to obtain the device's phone number from Alibaba Cloud. The app completes "one-tap login" through the number verification mobile SDK and sends the resulting token to your backend; the backend calls this package and gets the phone number.

Integration testing status

Real Alibaba Cloud number verification 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, concurrency safety, and the "unavailable when credentials aren't configured" semantics.

One more item is pending confirmation: the phone number in Alibaba Cloud's documentation examples is masked, and it hasn't been verified whether the real response returns the full number or a masked one. This package doesn't transform the number in any way and passes it through as-is; confirm this in your own integration testing before deciding whether it can be used directly as an account key.

Basics ​

ItemValue
API calledThe number verification service's GetMobile operation; default endpoint https://dypnsapi.aliyuncs.com (constant DefaultEndpoint)
Signing regionDefaults to cn-hangzhou (constant DefaultRegionID)
DependenciesAlibaba Cloud Go SDK (the number verification service and error packages of github.com/aliyun/alibaba-cloud-sdk-go)
Entry pointsNewVerifier(PhoneAuthConfig) (Verifier, error); Verifier.Verify(ctx, token) (*VerifiedPhone, error)

Example ​

The following code has been compiled and run against a local stub service (stub omitted):

go
import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"os"

	"github.com/gonewx/wicket/phoneverify"
)

func newPhoneAuth() (phoneverify.Verifier, error) {
	return phoneverify.NewVerifier(phoneverify.PhoneAuthConfig{
		AppKey:          os.Getenv("PHONE_AUTH_APP_KEY"),
		AccessKeyID:     os.Getenv("PHONE_AUTH_ACCESS_KEY_ID"),
		AccessKeySecret: os.Getenv("PHONE_AUTH_ACCESS_KEY_SECRET"),
	})
}

func handleOneTapLogin(v phoneverify.Verifier, logger *slog.Logger, w http.ResponseWriter, r *http.Request) {
	phone, err := v.Verify(r.Context(), r.FormValue("token"))

	var vendorErr *phoneverify.VendorError
	if errors.As(err, &vendorErr) {
		// Already redacted and length-limited, safe to log directly for reconciliation and support tickets
		logger.Warn("phone auth rejected", "code", vendorErr.Code,
			"request_id", vendorErr.RequestID, "status", vendorErr.HTTPStatus)
	}

	switch {
	case err == nil:
		_ = phone.Mobile // find or create a local user by phone number
		w.WriteHeader(http.StatusOK)
	case errors.Is(err, phoneverify.ErrInvalidCredential):
		http.Error(w, "invalid token", http.StatusUnauthorized)
	case errors.Is(err, phoneverify.ErrRetryable), errors.Is(err, phoneverify.ErrUnavailable):
		// Both mean "can't decide right now"; never let the user through; 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)
	}
}

Configuration: PhoneAuthConfig ​

FieldDescription
AppKeyIdentifier of the verification scheme in the console. Required
AccessKeyIDAccess key ID. Required
AccessKeySecretAccess key secret. Required
RegionIDRegion used for signing; empty means cn-hangzhou
EndpointEndpoint override as a full URL (test stub, VPC); empty means DefaultEndpoint. Must be exactly http(s)://host[:port]; a path, query string, fragment, or user info is rejected (ErrUnavailable)
TimeoutRead and connect timeout for a single exchange; 10 seconds when ≤ 0

If any of the three required fields is empty (including whitespace only), NewVerifier returns ErrUnavailable.

AppKey is actually not sent with the request—the GetMobile operation has no such parameter; it identifies the verification scheme on the mobile SDK side. It's still required so that a deployment with "incomplete identity configuration" is rejected as a whole at startup, rather than failing only at whichever step happens to check it.

Successful result ​

go
type VerifiedPhone struct {
	Mobile    string // phone number, non-empty on success; passed through as-is with no format conversion
	RequestID string // Alibaba Cloud request ID, for reconciliation and support-ticket troubleshooting
}

Error categories ​

Check the four sentinels with errors.Is; get the vendor's raw return code, message, request ID, and HTTP status with errors.As on *VendorError (fields Code, Message, RequestID, HTTPStatus). All of these fields are redacted and length-limited.

SentinelTriggered by
ErrInvalidCredentialEmpty or whitespace-only token (no request sent); return codes isv.TOKEN_INVALID, isv.TOKEN_UNAUTHORIZED_USED, InvalidBizToken.Expired, InvalidBizToken.Check, GET_MOBILE_FAILED; any unrecognized return code (exception below)
ErrRetryableReturn codes isp.QPS_LIMIT, isv.BUSINESS_LIMIT_CONTROL, isp.SYSTEM_ERROR, isp.UNKNOWN; an unrecognized return code arriving with HTTP 5xx / 429 / 408; network transport failure; SDK timeout; ctx deadline expired
ErrMalformedResponseThe response can't be decoded (including a panic during SDK decoding, which is recovered); no return code; return code OK but an empty phone number
ErrUnavailableRequired fields not configured; invalid Endpoint; the SDK refuses to build or send the request; and configuration-type return codes on your side: isv.RAM_PERMISSION_DENY, isv.FORBIDDEN_ACTION, isv.PRODUCT_UNSUBSCRIBE, isv.VERIFY_SCHEME_NOT_EXIST, SignatureDoesNotMatch, IncompleteSignature

Some design trade-offs:

  • Configuration-type return codes map to ErrUnavailable. Insufficient permissions, a product that isn't activated, or a signature mismatch say "something is wrong with your credentials or activation status", which is a different matter from "the user's token is invalid". If they were classified as invalid credentials, you'd return 401 to a perfectly normal user and hide an operations failure behind the user. Classified as unavailable, the user still isn't let through, but the cause is attributed correctly. (wechatverify doesn't make this distinction, because WeChat lacks an equally clear code table.)
  • Return codes are matched by exact string equality, not grouped by prefix such as isv.: the same prefix contains both credential-type and configuration-type codes.
  • Unrecognized codes default to the stricter ErrInvalidCredential; the only relaxation is that when one arrives with 5xx / 429 / 408 it's treated as ErrRetryable, because in that case the status code expresses a vendor-side failure.
  • Business failures arrive as HTTP 200. Alibaba Cloud signals failure with 200 + a non-OK return code, and this package checks the return code before giving a result. If you bypass this package and call the SDK directly, "no error means success" is the easiest trap to fall into.
  • An explicit vendor answer takes precedence over the context: once the vendor's error response has been received, even if the deadline happens to expire at that moment, the error is classified by the vendor's answer, to avoid treating an already-used token as retryable and submitting it again. Without a vendor answer, ctx cancellation satisfies errors.Is(err, context.Canceled) and isn't retryable; ctx deadline expiry satisfies both context.DeadlineExceeded and ErrRetryable. If ctx has already ended when Verify is called, it returns directly according to these rules without doing any work.

Concurrency and timeouts ​

  • A Verifier can be used concurrently by any number of goroutines.
  • The SDK client rewrites its own timeout and transport on every request, so sharing one client under concurrency causes data races. This package therefore creates a new client for every call and closes it afterwards; the overhead is far smaller than the network round trip that follows.
  • The SDK's GetMobile doesn't accept a context. This package wires the caller's ctx into the request through a custom transport, so cancellation actually interrupts the in-flight request instead of waiting for the transport timeout.
  • Timeouts are bounded by two layers together: PhoneAuthConfig.Timeout (10 seconds by default) and the caller ctx's deadline; whichever comes first wins.

Keeping secrets from leaking ​

Requests are signed with HMAC, so AccessKeySecret itself never goes over the network; but the signed request URL contains AccessKeyId, the user's token, and the signature, and on a transport failure the SDK's error includes the full URL. How this package handles it:

  • AccessKeySecret, AccessKeyID, and the current token in error text are all replaced with [REDACTED], matching three forms: raw, URL-encoded once, and URL-encoded twice (signature echo); the per-request Signature and SignatureNonce are stripped by parameter name. Redaction always happens before truncation, so secrets aren't cut into fragments that no longer match.
  • PhoneAuthConfig implements String(), GoString(), MarshalJSON(), and slog.LogValuer: in %v, %+v, %#v, json.Marshal, and slog output, AccessKeySecret is [REDACTED]. Deserialization is unaffected.
  • A deliberate asymmetry: the configuration's string form keeps AccessKeyID in plain text (it's an account identifier visible in the console, which helps operations recognize it); error text strips it, because there it sits alongside the signature in a replayable URL.
  • The package doesn't log, and turns off tracing for each SDK client—when the SDK detects a process-wide global tracer, it names spans with the full request URI, which would bypass the redaction above.

Outside this protection

  • The SDK's own DEBUG=sdk environment variable prints full requests to standard output; don't set it in production.
  • If you pull out the AccessKeySecret field yourself and print it, the package can't protect it.

Relationship to the container ​

The container registers an empty *phoneverify.PhoneAuthConfig singleton (root.Ptr_PhoneAuthConfig()) as a configuration extension point, but doesn't register a Verifier. Just construct the verifier directly in your initialization code. If you want to use this shared pointer, write it only during wiring, before handling requests; writing while requests are in flight causes data races, and clearing a field makes later constructions silently become ErrUnavailable.

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