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
| Item | Value |
|---|---|
| API called | The number verification service's GetMobile operation; default endpoint https://dypnsapi.aliyuncs.com (constant DefaultEndpoint) |
| Signing region | Defaults to cn-hangzhou (constant DefaultRegionID) |
| Dependencies | Alibaba Cloud Go SDK (the number verification service and error packages of github.com/aliyun/alibaba-cloud-sdk-go) |
| Entry points | NewVerifier(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):
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
| Field | Description |
|---|---|
AppKey | Identifier of the verification scheme in the console. Required |
AccessKeyID | Access key ID. Required |
AccessKeySecret | Access key secret. Required |
RegionID | Region used for signing; empty means cn-hangzhou |
Endpoint | Endpoint 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) |
Timeout | Read 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
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.
| Sentinel | Triggered by |
|---|---|
ErrInvalidCredential | Empty 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) |
ErrRetryable | Return 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 |
ErrMalformedResponse | The 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 |
ErrUnavailable | Required 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. (wechatverifydoesn'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 asErrRetryable, 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-
OKreturn 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,
ctxcancellation satisfieserrors.Is(err, context.Canceled)and isn't retryable;ctxdeadline expiry satisfies bothcontext.DeadlineExceededandErrRetryable. Ifctxhas already ended whenVerifyis called, it returns directly according to these rules without doing any work.
Concurrency and timeouts
- A
Verifiercan 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
GetMobiledoesn't accept acontext. This package wires the caller'sctxinto 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 callerctx'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-requestSignatureandSignatureNonceare stripped by parameter name. Redaction always happens before truncation, so secrets aren't cut into fragments that no longer match.PhoneAuthConfigimplementsString(),GoString(),MarshalJSON(), andslog.LogValuer: in%v,%+v,%#v,json.Marshal, andslogoutput,AccessKeySecretis[REDACTED]. Deserialization is unaffected.- A deliberate asymmetry: the configuration's string form keeps
AccessKeyIDin 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=sdkenvironment variable prints full requests to standard output; don't set it in production. - If you pull out the
AccessKeySecretfield 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.