pentaho/pentaho-kettle · error · KettleDatabaseException
CmsTokenProvider: Keycloak response did not contain…
Error message
CmsTokenProvider: Keycloak response did not contain 'access_token'
What it means
CmsTokenProvider throws this when it successfully received an HTTP response from the Keycloak token endpoint but the parsed JSON body has no 'access_token' field. It means the server responded, but not with a usable OAuth2 token grant, so no credential can be cached or returned.
Solutions
- Verify the tokenUrl points to the exact OIDC token endpoint: <server>/realms/<realm>/protocol/openid-connect/token
- Check the client credentials (client_id/client_secret or username/password grant) are valid with: curl -d 'grant_type=...&client_id=...' <tokenUrl> and inspect the returned JSON for 'access_token' or an 'error' field
- Log/inspect the full response body and HTTP status to see what Keycloak actually returned
- Confirm no proxy or gateway is rewriting the response into an HTML error page
Example fix
// before (opaque failure)
Object tokenObj = responseBody.get( "access_token" );
// after (surface the server-side error)
Object tokenObj = responseBody.get( "access_token" );
if ( tokenObj == null ) {
Object err = responseBody.get( "error" );
throw new KettleDatabaseException( "Keycloak response missing access_token; server said: "
+ ( err != null ? err + ": " + responseBody.get( "error_description" ) : responseBody ) );
} Defensive patterns
Strategy: validation
Validate before calling
// Pre-check via curl or Java: ensure the token endpoint returns a JSON grant
Map<String,Object> body = new ObjectMapper().readValue( httpClient.execute( tokenRequest ).getEntity().getContent(), Map.class );
if ( !body.containsKey( "access_token" ) ) {
throw new IllegalStateException( "Token endpoint returned error: " + body.get( "error" ) );
} Type guard
boolean hasToken( Map<String,Object> resp ) { return resp != null && resp.get( "access_token" ) instanceof String && !( (String) resp.get( "access_token" ) ).isEmpty(); } Try / catch
try { token = CmsTokenProvider.getToken(); } catch ( KettleDatabaseException e ) { log.error( "Keycloak grant missing access_token; check client credentials and tokenUrl", e ); throw new AuthenticationException( e ); } Prevention
- Validate tokenUrl resolves to the OIDC token endpoint (…/protocol/openid-connect/token) at startup
- Test client credentials with curl before wiring them into configuration
- Log the response body (minus secrets) on failure to see Keycloak's error field
- Ensure no proxy/gateway intercepts the token request
When it happens
Trigger: fetchAndCache() POSTs credentials to the Keycloak token URL, parses the entity as a Map via Jackson, and throws when responseBody.get("access_token") is null — e.g. the endpoint returned an OAuth2 error body like {"error":"invalid_grant"}, an HTML login/error page, or an empty body with HTTP 200.
Common situations: Wrong client_id/client_secret or expired user credentials in the Keycloak grant; tokenUrl pointing at a non-token endpoint (e.g. the realm page instead of /protocol/openid-connect/token); a proxy or SSO gateway intercepting the request and returning HTML; Keycloak realm misconfiguration.
Related errors
- CmsTokenProvider: failed to fetch token from
- CmsTokenProvider: Keycloak token request failed — HTTP
- Unable to get authorization token
- Attempting to create PDI Repository with no Active…
- Auth error
AI-assisted analysis of pentaho/pentaho-kettle@f3058517a1 (2026-09-13).
Data as JSON: /api/errors/e81394ac9913ecfc.
Report an issue: GitHub.
Appendix: source
Thrown at core/src/main/java/org/pentaho/di/core/database/CmsTokenProvider.java:159
request.setEntity( new StringEntity( body, StandardCharsets.UTF_8 ) );
var response = client.execute( request );
int status = response.getStatusLine().getStatusCode();
if ( status != HttpURLConnection.HTTP_OK ) {
throw new KettleDatabaseException(
"CmsTokenProvider: Keycloak token request failed — HTTP " + status
+ " from " + tokenUrl );
}
Map<?, ?> responseBody;
try ( java.io.InputStream is = response.getEntity().getContent() ) {
responseBody = new ObjectMapper().readValue( is, Map.class );
}
Object tokenObj = responseBody.get( "access_token" );
if ( tokenObj == null ) {
throw new KettleDatabaseException(
"CmsTokenProvider: Keycloak response did not contain 'access_token'" );
}
String accessToken = tokenObj.toString();
long expiresInMs = 300_000L; // default 5 min if field is absent
Object expiresInObj = responseBody.get( "expires_in" );
if ( expiresInObj instanceof Number ) {
expiresInMs = ( (Number) expiresInObj ).longValue() * 1000L;
}
long validUntilMs = System.currentTimeMillis() + expiresInMs - EXPIRY_BUFFER_MS;
cached.set( new TokenEntry( accessToken, validUntilMs ) );
log.logDebug( "CmsTokenProvider: token acquired, valid for ~" + ( expiresInMs / 1000 ) + "s" );
return accessToken;
} catch ( KettleDatabaseException e ) {
throw e;
} catch ( Exception e ) {View on GitHub (pinned to f3058517a1)