Local signature validation vs. server-side exchange
This page explains the two mechanisms behind Wicket's three stateless verification packages, and how to choose based on the shape of the credential you have.
What the three packages have in common: they're plain Go packages that your application backend calls directly. They don't go through Wicket's middleware, don't read or write any store, and don't depend on the DI container. You pass in a credential string obtained by a mobile app or frontend, and get back a verified identity or a classifiable error.
The two mechanisms
Local signature validation (oidcverify): the credential is a signed, self-contained ID token (JWT). The backend verifies the signature locally using the JWKS public keys published by the provider, then checks claims such as iss, aud, and exp. The public keys are cached, so the vast majority of verifications need no outbound network access.
Server-side exchange (wechatverify, phoneverify): the credential is a short-lived opaque string that only the vendor can interpret (a WeChat authorization code, a number verification token). The backend attaches its own application credentials, sends the string to the vendor's API, and the vendor returns the identity. Every verification is a network round trip.
Comparison
| Dimension | Local signature validation oidcverify | Server-side exchange wechatverify / phoneverify |
|---|---|---|
| Credential shape | Signed JWT carrying its own claims | Opaque short-lived string |
| Outbound network per verification? | No; only on the first JWKS fetch, on an unknown kid, and on cache-expiry refresh | Yes, one round trip each time |
| Typical latency | Local cryptography, sub-millisecond | Depends on the network round trip to the vendor |
| Impact of a vendor outage | Unaffected once public keys are cached | Can't verify while the vendor is unavailable |
| Secrets the backend must hold | None; only public keys | Yes: app secret / access key |
| Time checks | Your side: inject a clock to validate exp / iat / nbf | Vendor side: the vendor decides whether the credential has expired |
| Quotas and rate limits | None | Subject to the vendor's QPS and quotas; "retryable" is a routine branch |
| Verifying the same credential again | Possible, with the same result | Usually not; credentials are mostly single-use, and a repeated exchange fails |
| Error style | 9 fine-grained sentinels (signature, expiry, issuer, audience, nonce…) | 4 category sentinels + *VendorError carrying the vendor's original code |
How to choose
There's only one rule: look at the shape of the credential, not the vendor's name.
- The credential is a signed JWT (Apple's
identityToken, an ID token from the Google Sign-In SDK, an ID token from any standard OIDC provider) → useoidcverify. Even if the vendor also offers a "send me the token and I'll verify it" API, there's no need to use it: one more outbound call just turns the vendor's outages into your outages. - The credential is a vendor-private opaque string → server-side exchange is the only option, because nobody but the vendor can read it.
Conventions shared by both
Regardless of mechanism, all three packages take the same conservative stance:
- Always fail closed. Any situation where the identity can't be confirmed returns an error; there's no "verification failed but here's a partial identity".
- Construction fails when unconfigured. When required configuration is missing,
NewVerifierreturns an error immediately, rather than you finding out at the first verification.wechatverify/phoneverifyreturnErrUnavailable;oidcverifyreturns a plain error when the issuer or audience is empty. - Unreadable responses are treated as "rejected". In a server-side exchange, an unrecognized vendor return code is classified as
ErrInvalidCredentialrather thanErrRetryable—treating a rejection you can't read as retryable could retry it into an accidental pass. - Caller cancellation isn't retryable (
wechatverify/phoneverifyonly). Whenctxis canceled, the error satisfieserrors.Is(err, context.Canceled)and carries no retryable marker; when the deadline expires, it satisfies bothcontext.DeadlineExceededandErrRetryable.oidcverifydoesn't preserve context errors: cancellation or timeout while fetching JWKS both surface asErrKeyNotFound. - The packages don't log. Error values and the string forms of configuration are already scrubbed of secrets; logging is up to the caller.
Suggested response codes
The table below is one common way to map verification results to your own API responses, for reference:
| Result | Suggested response | Reason |
|---|---|---|
| Success | 200, continue the business flow | — |
ErrInvalidCredential, or any oidcverify sentinel other than ErrKeyNotFound | 401 | The credential the user submitted doesn't hold up |
oidcverify.ErrKeyNotFound | 401 or 503 | Either the token's kid really doesn't exist, or fetching JWKS failed; the sentinel alone can't tell them apart, so alert and check the logs |
ErrRetryable | 503, optionally with Retry-After | Can't decide right now; the client may retry later |
ErrUnavailable | 503, and alert | A configuration or permission problem on your side, for operations to handle |
ErrMalformedResponse | 502 | The vendor returned something unintelligible |
context.Canceled | No response needed | The caller has given up |
Typical mobile login sequence (server-side exchange)
App ──(vendor SDK)──> Vendor: user authorizes
App <──────────────── Vendor: one-time code / token
App ──(code)────────> Your backend
Your backend ──(code + app credentials)──> Vendor API
Your backend <──(unionid / phone number)── Vendor API
App <──(your session)── Your backend: find or create the user by identityThe app secret lives only on your backend (and in the process hosting Wicket). It doesn't need to be shipped to the app or distributed to every business system.