spring-projects/spring-security · error · BadJwtException

An error occurred while attempting to decode the Jwt: Malfor

Error message

An error occurred while attempting to decode the Jwt: Malformed payload

What it means

NimbusJwtDecoder.createJwt's final catch-all converts a generic failure whose cause is a ParseException into a BadJwtException with the message 'An error occurred while attempting to decode the Jwt: Malformed payload'. This indicates the JWT string itself could not be parsed as a JOSE object — its compact form is not valid base64url-encoded JSON parts.

Source

Thrown at oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java:189

					.claims((c) -> c.putAll(claims))
					.build();
			// @formatter:on
		}
		catch (RemoteKeySourceException ex) {
			this.logger.trace("Failed to retrieve JWK set", ex);
			if (ex.getCause() instanceof ParseException) {
				throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set"), ex);
			}
			throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
		catch (JOSEException ex) {
			this.logger.trace("Failed to process JWT", ex);
			throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
		catch (Exception ex) {
			this.logger.trace("Failed to process JWT", ex);
			if (ex.getCause() instanceof ParseException) {
				throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload"), ex);
			}
			throw new BadJwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
		}
	}

	private Jwt validateJwt(Jwt jwt) {
		OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
		if (result.hasErrors()) {
			Collection<OAuth2Error> errors = result.getErrors();
			String validationErrorString = getJwtValidationExceptionMessage(errors);
			throw new JwtValidationException(validationErrorString, errors);
		}
		return jwt;
	}

	private String getJwtValidationExceptionMessage(Collection<OAuth2Error> errors) {
		for (OAuth2Error oAuth2Error : errors) {
			if (StringUtils.hasLength(oAuth2Error.getDescription())) {

View on GitHub (pinned to 96852e8860)

Solutions

  1. Log the raw Authorization header and inspect the token: it must be three base64url segments separated by dots.
  2. Ensure the Bearer prefix is stripped and no whitespace/newlines are passed to decode().
  3. Verify the client is actually configured to obtain JWTs from the IdP (an opaque/reference token will not parse).
  4. Catch BadJwtException specifically to return 401 to the caller instead of 500.

Example fix

// before
String token = request.getHeader("Authorization"); // "Bearer eyJ..."
Jwt jwt = this.decoder.decode(token);
// after
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
    token = token.substring(7).trim();
}
Jwt jwt = this.decoder.decode(token);
Defensive patterns

Strategy: validation

Validate before calling

// Validate compact JWT form before calling decode
static boolean isPlausibleJwt(String token) {
    if (token == null || token.isBlank()) return false;
    String[] parts = token.split("\\.");
    if (parts.length != 3) return false;
    try {
        Base64.getUrlDecoder().decode(parts[0]);
        Base64.getUrlDecoder().decode(parts[1]);
        return true;
    } catch (IllegalArgumentException e) {
        return false;
    }
}

Try / catch

try {
    Jwt jwt = decoder.decode(token);
} catch (BadJwtException ex) {
    // malformed token -> always 401, never 500; do not retry
    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid token format");
}

Prevention

When it happens

Trigger: decode(encodedJwt) is called with a string that is not a syntactically valid JWT: wrong number of dot-separated segments, segments not valid base64url, payload not valid JSON (causing Nimbus' ParseException as the cause), or whitespace/garbage passed in place of the token.

Common situations: Client sends 'Bearer null'/'undefined' or an empty/truncated token in the Authorization header; a Bearer token with extra characters (e.g. 'Bearer' prefix not stripped) or a token from a different format (opaque token, not JWT) is handed to the decoder; copying a token with line breaks.

Understand the failure class

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/e2337554d5cbe56f. Report an issue: GitHub.