Error message
token signature is invalid: crypto/ecdsa: verification error
Also printed as
- token signature is invalid: crypto/rsa: verification error
- token signature is invalid: signature is invalid
- token signature is invalid: ed25519: verification error
- crypto/ecdsa: verification error
golang-jwt v4 returns only the inner text, e.g. "crypto/ecdsa: verification error". Use `errors.Is(err, jwt.ErrTokenSignatureInvalid)` (v5) instead of matching strings.
golang-jwt/jwt (Go) — jwt.ErrTokenSignatureInvalid joined with the algorithm's verification error
What it means
golang-jwt parsed the token, accepted its algorithm and obtained a key from your `Keyfunc`, but the signature did not verify with that key. In v5 the error joins `jwt.ErrTokenSignatureInvalid` ("token signature is invalid") with the algorithm's own error: `crypto/ecdsa: verification error` for ES*, `crypto/rsa: verification error` for RS*/PS*, `signature is invalid` for HS*. The token's bytes and the key simply do not belong together.
Why it happens
The Keyfunc returns the wrong key
commonA key from another environment, a hard-coded key from documentation, or a Keyfunc that ignores `kid` and always returns the first key of a multi-key set.
The issuer rotated its signing key
commonServices that load a public key once at startup keep verifying with the old key after a rotation. Every new token fails while older cached tokens still pass, until the service reloads the key.
The signer produced a DER-encoded ECDSA signature
occasionalgolang-jwt expects ES256 signatures as 64 raw bytes (r‖s, RFC 7518). A token signed by a KMS or by `ecdsa.SignASN1` without conversion carries a ~71-byte DER signature, which fails verification even with the right key.
The token was changed after signing
occasionalRe-encoded payloads, whitespace changes in the header or payload segment, or a token assembled from parts of two different tokens. The signature covers the exact base64url text.
How to fix it
- 1.
Resolve the key by kid from the JWKS
Let the Keyfunc pick the key named in the token header from the issuer's key set and refresh it on rotation (for example with `github.com/MicahParks/keyfunc`). Always pin `WithValidMethods`.
go import ( "context" "github.com/MicahParks/keyfunc/v3" "github.com/golang-jwt/jwt/v5" ) // Resolve the verification key from the issuer's JWKS by the token's kid. k, err := keyfunc.NewDefaultCtx(ctx, []string{"https://issuer.example.com/.well-known/jwks.json"}) if err != nil { return err } token, err := jwt.Parse(raw, k.Keyfunc, jwt.WithValidMethods([]string{"ES256"}), jwt.WithIssuer("https://issuer.example.com/"), jwt.WithAudience("https://api.example.com")) - 2.
Prove both sides use the same key
Hash the DER-encoded public key on the verifier and the public half of the issuer's private key. Different hashes end the investigation.
bash # Same key on both sides? Compare SPKI fingerprints. openssl pkey -pubin -in verifier-public.pem -outform DER | sha256sum openssl pkey -in issuer-private.pem -pubout -outform DER | sha256sum - 3.
Handle the error class, not the string
golang-jwt v5 exposes sentinel errors. Map a signature failure to 401, and do not fall back to trying other keys or skipping verification.
go switch { case errors.Is(err, jwt.ErrTokenSignatureInvalid): return http.StatusUnauthorized // wrong key or tampered token: never retry with another key blindly case errors.Is(err, jwt.ErrTokenExpired): return http.StatusUnauthorized // client should refresh case errors.Is(err, jwt.ErrTokenMalformed): return http.StatusBadRequest }