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
12 changes: 12 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_5_7.adoc
Original file line number Diff line number Diff line change
@@ -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}].
4 changes: 4 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,20 +572,20 @@ public List<ProviderConfigProperty> getConfigMetadata() {
.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()
Expand All @@ -603,8 +603,8 @@ public List<ProviderConfigProperty> getConfigMetadata() {
.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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,7 @@ public static Stream<ClientScopeModel> getRequestedClientScopes(KeycloakSession

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 -> {
Expand All @@ -726,13 +727,13 @@ public static Stream<ClientScopeModel> getRequestedClientScopes(KeycloakSession
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);

Expand All @@ -744,10 +745,14 @@ private static ClientScopeModel tryResolveDynamicClientScope(KeycloakSession ses
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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -43,8 +45,10 @@
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;
Expand Down Expand Up @@ -139,6 +143,8 @@ public Response processGrantRequest() {
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.
Expand Down Expand Up @@ -226,6 +232,29 @@ private void checkParameterDuplicated() {
}
}

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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@
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;
Expand Down Expand Up @@ -654,6 +656,34 @@ public void testClientScopesPermissions() {
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();
Expand Down
Loading