mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-24 10:48:52 -05:00
Improve performance of scope processing in TokenManager. Limit for maximum length of OIDC parameters in Token endpoint (#478) (#47799)
closes #47716
Closes CVE-2026-4634
(cherry picked from commit b455ee4f28)
Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
== Notable changes
|
||||
|
||||
Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
|
||||
It also lists significant changes to internal APIs.
|
||||
|
||||
=== Maximum length of the parameters in the OIDC token endpoint
|
||||
|
||||
When the OIDC token endpoint request (or OAuth2 token endpoint request) is sent, a new limit exists for the maximum length of every OIDC/OAuth2 parameter. The maximum length of each parameter is 4,000 characters,
|
||||
which is aligned with the same limit, which already exists for the parameters sent to OIDC/OAuth authentication request.
|
||||
|
||||
If you want to increase or lower those numbers, start the server with the option `req-params-default-max-size` for the default maximum length of the
|
||||
OIDC/OAuth2 parameters or you can use something such as `req-params-max-size` for one specific parameter. For more details, see the `login-protocol` provider configuration in the link:{allproviderconfigguide_link}[{allproviderconfigguide_name}].
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
include::changes-26_6_0.adoc[leveloffset=2]
|
||||
|
||||
=== Migrating to 26.5.7
|
||||
|
||||
include::changes-26_5_7.adoc[leveloffset=2]
|
||||
|
||||
=== Migrating to 26.5.5
|
||||
|
||||
include::changes-26_5_5.adoc[leveloffset=2]
|
||||
|
||||
@@ -572,20 +572,20 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
|
||||
.property()
|
||||
.name(CONFIG_OIDC_REQ_PARAMS_DEFAULT_MAX_SIZE)
|
||||
.type("int")
|
||||
.helpText("Maximum default length of the standard OIDC parameter sent to the OIDC authentication request. This applies to most of the standard parameters like for example 'state', 'nonce' etc." +
|
||||
.helpText("Maximum default length of the standard OIDC parameter sent to the OIDC authentication or token endpoints. This applies to most of the standard parameters like for example 'state', 'nonce' etc." +
|
||||
" The exception is 'login_hint' parameter, which has maximum length of 255 characters.")
|
||||
.defaultValue(DEFAULT_REQ_PARAMS_DEFAULT_MAX_SIZE)
|
||||
.add()
|
||||
.property()
|
||||
.name(CONFIG_OIDC_REQ_PARAMS_MAX_SIZE_PREFIX + "--" + OIDCLoginProtocol.LOGIN_HINT_PARAM)
|
||||
.type("int")
|
||||
.helpText("Maximum length of the standard OIDC authentication request parameter overriden for the specified parameter. Useful if some standard OIDC parameter should have different limit than '" + CONFIG_OIDC_REQ_PARAMS_DEFAULT_MAX_SIZE +
|
||||
.helpText("Maximum length of the standard parameter sent to OIDC authentication or token endpoints overriden for the specified parameter. Useful if some standard OIDC parameter should have different limit than '" + CONFIG_OIDC_REQ_PARAMS_DEFAULT_MAX_SIZE +
|
||||
"'. It is needed to add the name of the parameter after this prefix into the configuration. In this example, the '" + OIDCLoginProtocol.LOGIN_HINT_PARAM + "' parameter is used, but this format is supported for any known standard OIDC/OAuth2 parameter.")
|
||||
.add()
|
||||
.property()
|
||||
.name(CONFIG_OIDC_ADD_REQ_PARAMS_MAX_NUMBER)
|
||||
.type("int")
|
||||
.helpText("Maximum number of additional request parameters sent to the OIDC authentication request. As 'additional request parameter' is meant some custom parameter not directly treated as standard OIDC/OAuth2 protocol parameter. Additional parameters might be useful for example to add custom claims to the OIDC token (in case that also particular protocol mappers are configured).")
|
||||
.helpText("Maximum number of additional request parameters sent to the OIDC authentication or token endpoints. As 'additional request parameter' is meant some custom parameter not directly treated as standard OIDC/OAuth2 protocol parameter. Additional parameters might be useful for example to add custom claims to the OIDC token (in case that also particular protocol mappers are configured).")
|
||||
.defaultValue(DEFAULT_ADDITIONAL_REQ_PARAMS_MAX_NUMBER)
|
||||
.add()
|
||||
.property()
|
||||
@@ -603,8 +603,8 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
|
||||
.property()
|
||||
.name(CONFIG_OIDC_ADD_REQ_PARAMS_FAIL_FAST)
|
||||
.type("boolean")
|
||||
.helpText("Whether the fail-fast strategy should be enforced in case if the limit for some standard OIDC parameter or additional OIDC parameter is not met for the parameters sent to the OIDC authentication request." +
|
||||
" If false, then all additional request parameters to not meet the configuration are silently ignored. If true, an exception will be raised and OIDC authentication request will not be allowed.")
|
||||
.helpText("Whether the fail-fast strategy should be enforced in case if the limit for some standard OIDC parameter or additional OIDC parameter is not met for the parameters sent to the OIDC authentication or token endpoints." +
|
||||
" If false, then all additional request parameters to not meet the configuration are silently ignored. If true, an exception will be raised and request to the OIDC authentication or token endpoints will not be allowed.")
|
||||
.defaultValue(DEFAULT_ADDITIONAL_REQ_PARAMS_FAIL_FAST)
|
||||
.add()
|
||||
.build();
|
||||
|
||||
@@ -717,6 +717,7 @@ public class TokenManager {
|
||||
|
||||
Map<String, ClientScopeModel> allOptionalScopes = client.getClientScopes(false);
|
||||
|
||||
OrganizationScope orgScope = tryResolveOrganizationScope(session, scopeParam, user);
|
||||
// Add optional client scopes requested by scope parameter
|
||||
return Stream.concat(parseScopeParameter(scopeParam)
|
||||
.map(name -> {
|
||||
@@ -726,13 +727,13 @@ public class TokenManager {
|
||||
return scope;
|
||||
}
|
||||
|
||||
return tryResolveDynamicClientScope(session, scopeParam, user, name);
|
||||
return tryResolveOrganizationClientScope(session, user, orgScope, name);
|
||||
})
|
||||
.filter(Objects::nonNull),
|
||||
clientScopes).distinct();
|
||||
}
|
||||
|
||||
private static ClientScopeModel tryResolveDynamicClientScope(KeycloakSession session, String scopeParam, UserModel user, String name) {
|
||||
private static OrganizationScope tryResolveOrganizationScope(KeycloakSession session, String scopeParam, UserModel user) {
|
||||
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
|
||||
OrganizationScope orgScope = OrganizationScope.valueOfScope(session, scopeParam);
|
||||
|
||||
@@ -744,10 +745,14 @@ public class TokenManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
return orgScope.toClientScope(name, user, session);
|
||||
return orgScope;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
private static ClientScopeModel tryResolveOrganizationClientScope(KeycloakSession session, UserModel user, OrganizationScope orgScope, String name) {
|
||||
return orgScope == null ? null : orgScope.toClientScope(name, user, session);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.keycloak.protocol.oidc.endpoints;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
@@ -43,8 +45,10 @@ import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCProviderConfig;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.protocol.oidc.grants.OAuth2GrantType;
|
||||
import org.keycloak.protocol.oidc.token.TokenInterceptorException;
|
||||
@@ -139,6 +143,8 @@ public class TokenEndpoint {
|
||||
checkParameterDuplicated();
|
||||
}
|
||||
|
||||
checkParameters();
|
||||
|
||||
/*
|
||||
* To request an access token that is bound to a public key using DPoP, the client MUST provide a valid DPoP
|
||||
* proof JWT in a DPoP header when making an access token request to the authorization server's token endpoint.
|
||||
@@ -226,6 +232,29 @@ public class TokenEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
protected void checkParameters() {
|
||||
OIDCLoginProtocol loginProtocol = (OIDCLoginProtocol) session.getProvider(LoginProtocol.class, OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
OIDCProviderConfig config = loginProtocol.getConfig();
|
||||
|
||||
Map<String, List<String>> paramsCopy = new HashMap<>(formParams);
|
||||
for (Map.Entry<String, List<String>> param : paramsCopy.entrySet()) {
|
||||
String paramName = param.getKey();
|
||||
int totalLengthOfParamValues = param.getValue().stream()
|
||||
.map(String::length)
|
||||
.reduce(0, Integer::sum);
|
||||
int maxLength = config.getMaxLengthForTheParameter(paramName);
|
||||
if (totalLengthOfParamValues > maxLength) {
|
||||
logger.warnf("The size of OIDC parameter '%s' is longer (%d) than allowed (%d). %s", paramName, totalLengthOfParamValues, maxLength, config.isAdditionalReqParamsFailFast() ? "Request not allowed." : "Ignoring the parameter.");
|
||||
if (config.isAdditionalReqParamsFailFast()) {
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "The size of OIDC parameter '" + paramName + "' is longer than allowed.",
|
||||
Response.Status.BAD_REQUEST);
|
||||
} else {
|
||||
formParams.remove(paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TokenExchangeSamlProtocol extends SamlProtocol {
|
||||
|
||||
final SamlClient samlClient;
|
||||
|
||||
+30
@@ -53,11 +53,13 @@ import org.keycloak.testsuite.util.ClientManager;
|
||||
import org.keycloak.testsuite.util.RoleBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.keycloak.OAuthErrorException.INVALID_SCOPE;
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -654,6 +656,34 @@ public class OIDCScopeTest extends AbstractOIDCScopeTest {
|
||||
testApp.removeOptionalClientScope(scopeParentId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLengthyScopeParameter() {
|
||||
// Scope parameter too long (longer than 4000 characters). Will be ignored
|
||||
String scope = getLongScopeParameter(1000);
|
||||
oauth.scope(scope);
|
||||
AccessTokenResponse response = oauth.doPasswordGrantRequest("john", "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
AccessToken token = oauth.verifyToken(response.getAccessToken());
|
||||
Assert.assertFalse(TokenUtil.isOIDCRequest(token.getScope()));
|
||||
|
||||
// Scope parameter relatively long. Should not be ignored
|
||||
scope = getLongScopeParameter(800);
|
||||
oauth.scope(scope);
|
||||
response = oauth.doPasswordGrantRequest("john", "password");
|
||||
assertEquals(400, response.getStatusCode());
|
||||
assertEquals(INVALID_SCOPE, response.getError());
|
||||
}
|
||||
|
||||
// Get very long "scope" parameter created from big list of some unknown scopes
|
||||
private String getLongScopeParameter(int scopesCount) {
|
||||
StringBuilder scopeParam = new StringBuilder("openid");
|
||||
for (int i = 0 ; i < scopesCount ; i++) {
|
||||
scopeParam.append(" s").append(i);
|
||||
}
|
||||
String scope = scopeParam.toString();
|
||||
getLogger().infof("Scopes count: %d, Scope param length: %d", scopesCount, scope.length());
|
||||
return scope;
|
||||
}
|
||||
|
||||
private void testLoginAndClientScopesPermissions(String username, String expectedRoleScopes, String... expectedRoles) {
|
||||
String userId = ApiUtil.findUserByUsername(testRealm(), username).getId();
|
||||
|
||||
Reference in New Issue
Block a user