go-kit/kit · error · ErrUnexpectedSigningMethod
unexpected signing method
Error message
unexpected signing method
What it means
Returned from inside the keyfunc used by jwt.NewParser when token.Method != the signing method passed to NewParser (middleware.go:109-111). It then surfaces to the caller via the ValidationError.Inner path. Its security purpose is to stop algorithm confusion: the verifier must pin the expected algorithm rather than trust the token header.
Source
Thrown at auth/jwt/middleware.go:47
// middleware's context.
ErrTokenContextMissing = errors.New("token up for parsing was not passed through the context")
// ErrTokenInvalid denotes a token was not able to be validated.
ErrTokenInvalid = errors.New("JWT was invalid")
// ErrTokenExpired denotes a token's expire header (exp) has since passed.
ErrTokenExpired = errors.New("JWT is expired")
// ErrTokenMalformed denotes a token was not formatted as a JWT.
ErrTokenMalformed = errors.New("JWT is malformed")
// ErrTokenNotActive denotes a token's not before header (nbf) is in the
// future.
ErrTokenNotActive = errors.New("token is not valid yet")
// ErrUnexpectedSigningMethod denotes a token was signed with an unexpected
// signing method.
ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
)
// NewSigner creates a new JWT generating middleware, specifying key ID,
// signing string, signing method and the claims you would like it to contain.
// Tokens are signed with a Key ID header (kid) which is useful for determining
// the key to use for parsing. Particularly useful for clients.
func NewSigner(kid string, key []byte, method jwt.SigningMethod, claims jwt.Claims) endpoint.Middleware {
return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
token := jwt.NewWithClaims(method, claims)
token.Header["kid"] = kid
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(key)
if err != nil {
return nil, err
}
ctx = context.WithValue(ctx, JWTContextKey, tokenString)View on GitHub (pinned to 78fbbceece)
Solutions
- Align both sides: pass the SAME jwt.SigningMethod* constant to jwt.NewSigner and jwt.NewParser
- Never take the algorithm from the token header — always pin it in the parser configuration
- If multiple algorithms are legitimately in use, compare token.Method.Alg() against an allowlist inside your keyfunc
- Redeploy both signer and parser together when rotating algorithms
Example fix
// before: signer signs HMAC, parser pins RSA
signerMW := jwt.NewSigner("k1", hmacKey, jwt.SigningMethodHS256, claims)
parserMW := jwt.NewParser(kf, jwt.SigningMethodRS256, jwt.MapClaimsFactory)
// after: same method on both sides
parserMW := jwt.NewParser(kf, jwt.SigningMethodHS256, jwt.MapClaimsFactory) Defensive patterns
Strategy: validation
Validate before calling
// at startup, assert signer and parser share one algorithm constant
var allowedAlgs = map[string]bool{"HS256": true, "RS256": true}
func parserConfigOK(method jwt.SigningMethod) bool {
return allowedAlgs[method.Alg()]
} Type guard
func isExpectedMethod(token *jwt.Token, want jwt.SigningMethod) bool {
return token.Method.Alg() == want.Alg()
} Try / catch
if _, err := ep(ctx, req); err != nil {
if errors.Is(err, jwt.ErrUnexpectedSigningMethod) {
// 401 + security alert: alg mismatch is a classic token-tampering signal — never auto-retry
}
} Prevention
- Define the signing method once in shared config and pass it to both NewSigner and NewParser
- Never derive the verification algorithm from the untrusted token header
- Deploy algorithm changes atomically across signer and verifier
When it happens
Trigger: Signer uses jwt.SigningMethodHS256 but NewParser was configured with jwt.SigningMethodRS256 (or vice versa); multi-service setups where one service signs with a different alg than the verifier expects; an attacker crafts a token with alg switched (e.g. to 'none' or HS vs RS confusion) and the pinned-method check fires; refactoring changed the method on one side only.
Common situations: Client and server auth config drift between deploys; example code copied with RS256 while the existing signer uses HMAC; rotating from HMAC to RSA keys but only one side deployed; keyfunc that fails to inspect the alg at all (the go-kit default check catches it here).
Related errors
- token up for parsing was not passed through the context
- JWT was invalid
- JWT is expired
- JWT is malformed
- token is not valid yet
AI-assisted analysis of go-kit/kit@78fbbceece (2026-08-15).
Data as JSON: /api/errors/2817f8d971a0e44f.
Report an issue: GitHub.