{"record":{"id":"e81394ac9913ecfc","repo":"pentaho/pentaho-kettle","slug":"cmstokenprovider-keycloak-response-did-not-contain-access","errorCode":null,"errorMessage":"CmsTokenProvider: Keycloak response did not contain 'access_token'","messagePattern":"CmsTokenProvider: Keycloak response did not contain 'access_token'","errorType":"exception","errorClass":"KettleDatabaseException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/pentaho/di/core/database/CmsTokenProvider.java","lineNumber":159,"sourceCode":"      request.setEntity( new StringEntity( body, StandardCharsets.UTF_8 ) );\n\n      var response = client.execute( request );\n\n      int status = response.getStatusLine().getStatusCode();\n      if ( status != HttpURLConnection.HTTP_OK ) {\n        throw new KettleDatabaseException(\n          \"CmsTokenProvider: Keycloak token request failed — HTTP \" + status\n            + \" from \" + tokenUrl );\n      }\n\n      Map<?, ?> responseBody;\n      try ( java.io.InputStream is = response.getEntity().getContent() ) {\n        responseBody = new ObjectMapper().readValue( is, Map.class );\n      }\n\n      Object tokenObj = responseBody.get( \"access_token\" );\n      if ( tokenObj == null ) {\n        throw new KettleDatabaseException(\n          \"CmsTokenProvider: Keycloak response did not contain 'access_token'\" );\n      }\n      String accessToken = tokenObj.toString();\n\n      long expiresInMs = 300_000L; // default 5 min if field is absent\n      Object expiresInObj = responseBody.get( \"expires_in\" );\n      if ( expiresInObj instanceof Number ) {\n        expiresInMs = ( (Number) expiresInObj ).longValue() * 1000L;\n      }\n      long validUntilMs = System.currentTimeMillis() + expiresInMs - EXPIRY_BUFFER_MS;\n\n      cached.set( new TokenEntry( accessToken, validUntilMs ) );\n      log.logDebug( \"CmsTokenProvider: token acquired, valid for ~\" + ( expiresInMs / 1000 ) + \"s\" );\n      return accessToken;\n\n    } catch ( KettleDatabaseException e ) {\n      throw e;\n    } catch ( Exception e ) {","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/pentaho/pentaho-kettle/blob/f3058517a153da500bf4551f46d79b91bf8ec552/core/src/main/java/org/pentaho/di/core/database/CmsTokenProvider.java#L141-L177","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (opaque failure)\nObject tokenObj = responseBody.get( \"access_token\" );\n// after (surface the server-side error)\nObject tokenObj = responseBody.get( \"access_token\" );\nif ( tokenObj == null ) {\n  Object err = responseBody.get( \"error\" );\n  throw new KettleDatabaseException( \"Keycloak response missing access_token; server said: \"\n    + ( err != null ? err + \": \" + responseBody.get( \"error_description\" ) : responseBody ) );\n}","handlingStrategy":"validation","validationCode":"// Pre-check via curl or Java: ensure the token endpoint returns a JSON grant\nMap<String,Object> body = new ObjectMapper().readValue( httpClient.execute( tokenRequest ).getEntity().getContent(), Map.class );\nif ( !body.containsKey( \"access_token\" ) ) {\n  throw new IllegalStateException( \"Token endpoint returned error: \" + body.get( \"error\" ) );\n}","typeGuard":"boolean hasToken( Map<String,Object> resp ) { return resp != null && resp.get( \"access_token\" ) instanceof String && !( (String) resp.get( \"access_token\" ) ).isEmpty(); }","tryCatchPattern":"try { token = CmsTokenProvider.getToken(); } catch ( KettleDatabaseException e ) { log.error( \"Keycloak grant missing access_token; check client credentials and tokenUrl\", e ); throw new AuthenticationException( e ); }","preventionTips":["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"],"tags":["keycloak","oauth2","http-response","authentication"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"f3058517a153da500bf4551f46d79b91bf8ec552","analyzedAt":"2026-09-13T14:04:16.340Z","contentChangedAt":"2026-09-13T14:04:16.340Z","schemaVersion":2},"datasetVersion":"2026-09-20T23:17:15.980Z"}