Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-25_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ The nonce claim is now only added to the ID token strictly following the OpenID

A new `Nonce backwards compatible` mapper is also included in the software that can be assigned to client scopes to revert to the old behavior. For example, the JS adapter checked the returned `nonce` claim in all the tokens before fixing issue https://github.com/keycloak/keycloak/issues/26651[#26651] in version 24.0.0. Therefore, if an old version of the JS adapter is used, the mapper should be added to the required clients by using client scopes.

= Limiting memory usage when consuming HTTP responses

In some scenarios like brokering Keycloak uses HTTP to talk to external servers.
To avoid a denial of service when those providers send too much data, {project_name} now restricts responses to 10 MB by default.

Users can configure this limit by setting the provider configuration option `spi-connections-http-client-default-max-consumed-response-size`:

.Restricting the consumed responses to 1 MB
[source,bash]
----
bin/kc.[sh|bat] --spi-connections-http-client-default-max-consumed-response-size=1000000
----


= Removed a model module

The module `org.keycloak:keycloak-model-legacy` module was deprecated in a previous release and is removed in this release. Use the `org.keycloak:keycloak-model-storage` module instead.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.common.util.Base64;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.connections.httpclient.SafeInputStream;
import org.keycloak.models.KeycloakSession;
import org.keycloak.util.JsonSerialization;

Expand Down Expand Up @@ -86,48 +87,54 @@ public class SimpleHttp {

private int connectionRequestTimeoutMillis = UNDEFINED_TIMEOUT;

private long maxConsumedResponseSize;

private RequestConfig.Builder requestConfigBuilder;

protected SimpleHttp(String url, String method, HttpClient client) {
protected SimpleHttp(String url, String method, HttpClient client, long maxConsumedResponseSize) {
this.client = client;
this.url = url;
this.method = method;
this.maxConsumedResponseSize = maxConsumedResponseSize;
}

public static SimpleHttp doDelete(String url, KeycloakSession session) {
return doDelete(url, session.getProvider(HttpClientProvider.class).getHttpClient());
HttpClientProvider provider = session.getProvider(HttpClientProvider.class);
return doDelete(url, provider.getHttpClient(), provider.getMaxConsumedResponseSize());
}

public static SimpleHttp doDelete(String url, HttpClient client) {
return new SimpleHttp(url, "DELETE", client);
protected static SimpleHttp doDelete(String url, HttpClient client, long maxConsumedResponseSize) {
return new SimpleHttp(url, "DELETE", client, maxConsumedResponseSize);
}

public static SimpleHttp doGet(String url, KeycloakSession session) {
return doGet(url, session.getProvider(HttpClientProvider.class).getHttpClient());
HttpClientProvider provider = session.getProvider(HttpClientProvider.class);
return doGet(url, provider.getHttpClient(), provider.getMaxConsumedResponseSize());
}

public static SimpleHttp doGet(String url, HttpClient client) {
return new SimpleHttp(url, "GET", client);
protected static SimpleHttp doGet(String url, HttpClient client, long maxConsumedResponseSize) {
return new SimpleHttp(url, "GET", client, maxConsumedResponseSize);
}

public static SimpleHttp doPost(String url, KeycloakSession session) {
return doPost(url, session.getProvider(HttpClientProvider.class).getHttpClient());
HttpClientProvider provider = session.getProvider(HttpClientProvider.class);
return doPost(url, provider.getHttpClient(), provider.getMaxConsumedResponseSize());
}

public static SimpleHttp doPost(String url, HttpClient client) {
return new SimpleHttp(url, "POST", client);
protected static SimpleHttp doPost(String url, HttpClient client, long maxConsumedResponseSize) {
return new SimpleHttp(url, "POST", client, maxConsumedResponseSize);
}

public static SimpleHttp doPut(String url, HttpClient client) {
return new SimpleHttp(url, "PUT", client);
protected static SimpleHttp doPut(String url, HttpClient client, long maxConsumedResponseSize) {
return new SimpleHttp(url, "PUT", client, maxConsumedResponseSize);
}

public static SimpleHttp doHead(String url, HttpClient client) {
return new SimpleHttp(url, "HEAD", client);
protected static SimpleHttp doHead(String url, HttpClient client, long maxConsumedResponseSize) {
return new SimpleHttp(url, "HEAD", client, maxConsumedResponseSize);
}

public static SimpleHttp doPatch(String url, HttpClient client) {
return new SimpleHttp(url, "PATCH", client);
protected static SimpleHttp doPatch(String url, HttpClient client, long maxConsumedResponseSize) {
return new SimpleHttp(url, "PATCH", client, maxConsumedResponseSize);
}

public SimpleHttp header(String name, String value) {
Expand Down Expand Up @@ -178,6 +185,11 @@ public SimpleHttp connectionRequestTimeoutMillis(int timeout) {
return this;
}

public SimpleHttp setMaxConsumedResponseSize(long maxConsumedResponseSize) {
this.maxConsumedResponseSize = maxConsumedResponseSize;
return this;
}

public SimpleHttp auth(String token) {
header("Authorization", "Bearer " + token);
return this;
Expand Down Expand Up @@ -296,7 +308,7 @@ private Response makeRequest() throws IOException {
httpRequest.setConfig(requestConfigBuilder.build());
}

return new Response(client.execute(httpRequest));
return new Response(client.execute(httpRequest), maxConsumedResponseSize);
}

private RequestConfig.Builder requestConfigBuilder() {
Expand Down Expand Up @@ -341,12 +353,14 @@ private UrlEncodedFormEntity getFormEntityFromParameter() throws IOException{
public static class Response implements AutoCloseable {

private final HttpResponse response;
private final long maxConsumedResponseSize;
private int statusCode = -1;
private String responseString;
private ContentType contentType;

public Response(HttpResponse response) {
public Response(HttpResponse response, long maxConsumedResponseSize) {
this.response = response;
this.maxConsumedResponseSize = maxConsumedResponseSize;
}

private void readResponse() throws IOException {
Expand All @@ -368,6 +382,8 @@ private void readResponse() throws IOException {
}
}

is = new SafeInputStream(is, maxConsumedResponseSize);

try (InputStreamReader reader = charset == null ? new InputStreamReader(is, StandardCharsets.UTF_8) :
new InputStreamReader(is, charset)) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ public interface HttpClientProvider extends Provider {
* Helper method to retrieve the contents of a URL as a String.
* Decoding response with the correct character set is performed according to the headers returned in the server's response.
* To retrieve binary data, use {@link #getInputStream(String)}
*
* Implementations should limit the amount of data returned to avoid an {@link OutOfMemoryError}.
*
* @param uri URI with data to receive.
* @return Body of the response as a String.
Expand Down Expand Up @@ -90,4 +92,15 @@ default InputStream get(String uri) throws IOException {
return getInputStream(uri);
}

long DEFAULT_MAX_CONSUMED_RESPONSE_SIZE = 10_000_000L;

/**
* Get the configured limit for the response size.
*
* @return number of bytes
*/
default long getMaxConsumedResponseSize() {
return DEFAULT_MAX_CONSUMED_RESPONSE_SIZE;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.keycloak.connections.httpclient;

import java.io.IOException;
import java.io.InputStream;

/**
* Limit the amount of data read to prevent a {@link OutOfMemoryError}.
*
* @author Alexander Schwartz
*/
public class SafeInputStream extends InputStream {

private long bytesConsumed;
private final InputStream delegate;
private final long maxBytesToConsume;

public SafeInputStream(InputStream delegate, long maxBytesToConsume) {
this.delegate = delegate;
this.maxBytesToConsume = maxBytesToConsume;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
int sizeRead = delegate.read(b, off, len);
if (sizeRead > 0) {
bytesConsumed += sizeRead;
}
checkConsumedBytes();
return sizeRead;
}

private void checkConsumedBytes() throws IOException {
if (bytesConsumed > maxBytesToConsume) {
throw new IOException(String.format("Response is at least %s bytes in size, with max bytes to be consumed being %d", bytesConsumed, maxBytesToConsume));
}
}

@Override
public int read() throws IOException {
int result = delegate.read();
if (result > 0) {
++bytesConsumed;
}
checkConsumedBytes();
return result;
}

@Override
public void close() throws IOException {
delegate.close();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpPost;
Expand All @@ -20,7 +21,9 @@
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.connections.httpclient.HttpClientProvider;

import java.io.IOException;
import java.net.URLEncoder;
Expand All @@ -29,8 +32,11 @@
import java.util.Arrays;
import java.util.Collection;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;

/**
Expand Down Expand Up @@ -64,7 +70,7 @@ public static Collection<Object[]> entities() {
@Test
public void withCharset() throws IOException {
HttpResponse httpResponse = createBasicResponse(entity);
SimpleHttp.Response response = new SimpleHttp.Response(httpResponse);
SimpleHttp.Response response = new SimpleHttp.Response(httpResponse, HttpClientProvider.DEFAULT_MAX_CONSUMED_RESPONSE_SIZE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could add a test here too. I did something quick in this commit: rmartinc@1e65623
You can use it or something similar.

if (success) {
assertEquals(original, response.asString());
} else {
Expand All @@ -90,21 +96,39 @@ public RequestConsideringEncodingTest(String value) {

@Parameters(name = "{index}: requestWithEncoding({0})")
public static Collection<Object[]> entities() {
return Arrays.asList(new Object[][] { { "English" }, { "Русский" }, { "GermanÜmläütß" } });
return Arrays.asList(new Object[][] {
{ "English" },
{ "Русский" },
{ "GermanÜmläütß" },
{ SecretGenerator.getInstance().randomString(1000) },
{ SecretGenerator.getInstance().randomString(1024) }
});
}

@Test
public void requestWithEncoding() throws IOException {
String expectedResponse = "{\"value\":\"" + value + "\"}";
HttpClientMock client = new HttpClientMock();
SimpleHttp.doPost("", client).json(new DummyEntity(value)).asResponse();
assertEquals("{\"value\":\"" + value + "\"}", client.data);
if (expectedResponse.getBytes(StandardCharsets.UTF_8).length < 1024) {
SimpleHttp.Response response = SimpleHttp.doPost("", client, 1024).json(new DummyEntity(value)).asResponse();
assertEquals(expectedResponse, response.asString());
} else {
IOException e = assertThrows(IOException.class, () -> SimpleHttp.doPost("", client, 1024).json(new DummyEntity(value)).asResponse().asString());
assertThat(e.getMessage(), startsWith("Response is at least"));
}
}

@Test
public void requestWithEncodingParam() throws IOException {
String expectedResponse = "dummy=" + URLEncoder.encode(value, "UTF-8");
HttpClientMock client = new HttpClientMock();
SimpleHttp.doPost("", client).param("dummy", value).asResponse();
assertEquals("dummy=" + URLEncoder.encode(value, "UTF-8"), client.data);
if (expectedResponse.getBytes(StandardCharsets.UTF_8).length < 1024) {
SimpleHttp.Response response = SimpleHttp.doPost("", client, 1024).param("dummy", value).asResponse();
assertEquals(expectedResponse, response.asString());
} else {
IOException e = assertThrows(IOException.class, () -> SimpleHttp.doPost("", client, 1024).json(new DummyEntity(value)).asResponse().asString());
assertThat(e.getMessage(), startsWith("Response is at least"));
}
}

public static final class DummyEntity {
Expand All @@ -119,8 +143,6 @@ public DummyEntity(String value) {
*/
public static final class HttpClientMock implements HttpClient {

String data;

@Override
public HttpParams getParams() {
fail(); return null;
Expand All @@ -132,50 +154,52 @@ public ClientConnectionManager getConnectionManager() {
}

@Override
public HttpResponse execute(HttpUriRequest paramHttpUriRequest) throws IOException, ClientProtocolException {
public HttpResponse execute(HttpUriRequest paramHttpUriRequest) throws IOException {
HttpPost post = (HttpPost) paramHttpUriRequest;
data = StreamUtil.readString(post.getEntity().getContent());
return null;
String content = StreamUtil.readString(post.getEntity().getContent(), StandardCharsets.UTF_8);
BasicHttpResponse httpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), HttpStatus.SC_OK, "OK");
httpResponse.setEntity(new StringEntity(content, StandardCharsets.UTF_8));
return httpResponse;
}

@Override
public HttpResponse execute(HttpUriRequest paramHttpUriRequest, HttpContext paramHttpContext)
throws IOException, ClientProtocolException {
throws IOException {
fail(); return null;
}

@Override
public HttpResponse execute(HttpHost paramHttpHost, HttpRequest paramHttpRequest) throws IOException, ClientProtocolException {
public HttpResponse execute(HttpHost paramHttpHost, HttpRequest paramHttpRequest) throws IOException {
fail(); return null;
}

@Override
public HttpResponse execute(HttpHost paramHttpHost, HttpRequest paramHttpRequest, HttpContext paramHttpContext)
throws IOException, ClientProtocolException {
throws IOException {
fail(); return null;
}

@Override
public <T> T execute(HttpUriRequest paramHttpUriRequest, ResponseHandler<? extends T> paramResponseHandler)
throws IOException, ClientProtocolException {
throws IOException {
fail(); return null;
}

@Override
public <T> T execute(HttpUriRequest paramHttpUriRequest, ResponseHandler<? extends T> paramResponseHandler,
HttpContext paramHttpContext) throws IOException, ClientProtocolException {
HttpContext paramHttpContext) throws IOException {
fail(); return null;
}

@Override
public <T> T execute(HttpHost paramHttpHost, HttpRequest paramHttpRequest, ResponseHandler<? extends T> paramResponseHandler)
throws IOException, ClientProtocolException {
throws IOException {
fail(); return null;
}

@Override
public <T> T execute(HttpHost paramHttpHost, HttpRequest paramHttpRequest, ResponseHandler<? extends T> paramResponseHandler,
HttpContext paramHttpContext) throws IOException, ClientProtocolException {
HttpContext paramHttpContext) throws IOException {
fail(); return null;
}

Expand Down
Loading