Billionmail/BillionMail · error

unexpected signing method: %v

Error message

unexpected signing method: %v

What it means

jwt.Parse rejects the token because its 'alg' header is not an HMAC method (HS256/HS384/HS512). This repo's keyfunc intentionally returns an error when the signing method type-asserts to anything other than *jwt.SigningMethodHMAC, a standard protection against algorithm-confusion attacks (e.g. RS256/none). The wrapped message 'failed to parse JWT' is then produced by the caller.

Source

Thrown at core/internal/service/batch_mail/jwt.go:154

		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(cfg.secret))
}

// ParseUnsubscribeJWT 解析退订JWT
func ParseUnsubscribeJWT(tokenString string) (*UnsubscribeClaims, error) {
	if tokenString == "" {
		return nil, errors.New("empty token string")
	}

	cfg := getConfig()

	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		// Validate signing method
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte(cfg.secret), nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to parse JWT: %w", err)
	}

	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
		result := &UnsubscribeClaims{}

		// Extract email
		if email, ok := claims["email"].(string); ok {
			result.Email = email
		} else {
			return nil, errors.New("JWT missing email claim")
		}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Sign the token with jwt.SigningMethodHS256 (or another HMAC variant) matching the secret in getConfig().secret
  2. Confirm the token producer uses the same batch_mail JWT flow (GenerateUnsubscribeJWT), not a different JWT issuer
  3. If asymmetric signing is genuinely needed, extend the keyfunc to accept that method and return the corresponding public key

Example fix

// before
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// after
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
Defensive patterns

Strategy: validation

Validate before calling

func isHMAC(token *jwt.Token) bool {
	_, ok := token.Method.(*jwt.SigningMethodHMAC)
	return ok
}
// call jwt.Parse first and check err via errors.As
var algErr interface{ Error() string }
_ = algErr

Type guard

func hasAlg(token *jwt.Token, want string) bool {
	alg, ok := token.Header["alg"].(string)
	return ok && alg == want
}

Prevention

When it happens

Trigger: A unsubscribe JWT was signed with a non-HMAC algorithm (e.g. RS256, ES256, or 'none'), or crafted/externally generated tokens hit ParseUnsubscribeJWT with a foreign alg header.

Common situations: Tokens minted by a different service using asymmetric keys; an attacker probing the endpoint with alg=none; a library upgrade where the default signing method changed; mixing the subscribe-confirm token flow with the unsubscribe token flow.

Related errors


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/2bdf2c993f5bbccf. Report an issue: GitHub.