spring-projects/spring-security · error · OAuth2AuthenticationException

invalid_token

invalid_token

Error message

DPoP access token is malformed.

What it means

DPoPAuthenticationConverter throws invalid_token when the Authorization header uses the DPoP scheme but its value does not match the expected single-token pattern (e.g. it contains whitespace or is empty after the scheme). Like the bearer equivalent, this is a structural parse failure of the access token before validation.

Source

Thrown at oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/authentication/DPoPAuthenticationConverter.java:73

	public @Nullable Authentication convert(HttpServletRequest request) {
		List<String> authorizationList = Collections.list(request.getHeaders(HttpHeaders.AUTHORIZATION));
		if (CollectionUtils.isEmpty(authorizationList)) {
			return null;
		}
		if (authorizationList.size() != 1) {
			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST,
					"Found multiple Authorization headers.", null);
			throw new OAuth2AuthenticationException(error);
		}
		String authorization = authorizationList.get(0);
		if (!StringUtils.startsWithIgnoreCase(authorization, OAuth2AccessToken.TokenType.DPOP.getValue())) {
			return null;
		}
		Matcher matcher = AUTHORIZATION_PATTERN.matcher(authorization);
		if (!matcher.matches()) {
			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "DPoP access token is malformed.",
					null);
			throw new OAuth2AuthenticationException(error);
		}
		String accessToken = matcher.group("token");
		List<String> dPoPProofList = Collections.list(request.getHeaders(OAuth2AccessToken.TokenType.DPOP.getValue()));
		if (CollectionUtils.isEmpty(dPoPProofList) || dPoPProofList.size() != 1) {
			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, "DPoP proof is missing or invalid.",
					null);
			throw new OAuth2AuthenticationException(error);
		}
		String dPoPProof = dPoPProofList.get(0);
		return new DPoPAuthenticationToken(accessToken, dPoPProof, request.getMethod(),
				request.getRequestURL().toString());
	}

}

View on GitHub (pinned to 96852e8860)

Solutions

  1. Send exactly 'Authorization: DPoP <single-token>' with no internal whitespace
  2. Trim the token and any surrounding whitespace before setting the header
  3. Regenerate the token if it contains characters outside the allowed token charset
  4. Log the raw header value to confirm what is actually being sent

Example fix

// before
request.setHeader("Authorization", "DPoP " + accessToken + " extra");
// after
request.setHeader("Authorization", "DPoP " + accessToken.trim());
Defensive patterns

Strategy: validation

Validate before calling

String auth = request.getHeader("Authorization");
if (auth != null && auth.regionMatches(true, 0, "DPoP ", 0, 5)) {
    String token = auth.substring(5).trim();
    if (token.isEmpty() || token.matches(".*\\s.*")) {
        throw new IllegalArgumentException("Malformed DPoP access token");
    }
}

Type guard

boolean isValidDpopHeader(String header) {
    return header != null && header.matches("(?i)^DPoP [!-~]+$");
}

Try / catch

try {
    Authentication auth = converter.convert(request);
} catch (OAuth2AuthenticationException e) {
    if (OAuth2ErrorCodes.INVALID_TOKEN.equals(e.getError().getErrorCode())) {
        response.setStatus(401);
        response.setHeader("WWW-Authenticate", "DPoP error=\"invalid_token\"");
    }
}

Prevention

When it happens

Trigger: Sending 'Authorization: DPoP' with a malformed value: token containing spaces, an empty token, or extra characters after the token. The prefix check (startsWithIgnoreCase "DPoP") passes but AUTHORIZATION_PATTERN.matches() fails.

Common situations: Tokens pasted with trailing whitespace/newlines; clients hand-building the header with string concatenation errors; migration from Bearer to DPoP scheme where the header construction wasn't updated correctly.

Understand the failure class

Related errors


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