KEYCLOAK-11996 Authorization Endpoint does not return an error when a request includes a parameter more than once (#6696)
Co-authored-by: stianst <stianst@gmail.com> Co-authored-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
parent
24c6e2ba08
commit
87cab778eb
7 changed files with 113 additions and 12 deletions
|
@ -55,6 +55,7 @@ import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -111,7 +112,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response process(MultivaluedMap<String, String> params) {
|
private Response process(MultivaluedMap<String, String> params) {
|
||||||
String clientId = params.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
String clientId = AuthorizationEndpointRequestParserProcessor.getClientId(event, session, params);
|
||||||
|
|
||||||
checkSsl();
|
checkSsl();
|
||||||
checkRealm();
|
checkRealm();
|
||||||
|
@ -125,6 +126,11 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||||
return errorResponse;
|
return errorResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.getInvalidRequestMessage() != null) {
|
||||||
|
event.error(Errors.INVALID_REQUEST);
|
||||||
|
return redirectErrorToClient(parsedResponseMode, Errors.INVALID_REQUEST, request.getInvalidRequestMessage());
|
||||||
|
}
|
||||||
|
|
||||||
if (!TokenUtil.isOIDCRequest(request.getScope())) {
|
if (!TokenUtil.isOIDCRequest(request.getScope())) {
|
||||||
ServicesLogger.LOGGER.oidcScopeMissing();
|
ServicesLogger.LOGGER.oidcScopeMissing();
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ import java.util.Map;
|
||||||
*/
|
*/
|
||||||
public class AuthorizationEndpointRequest {
|
public class AuthorizationEndpointRequest {
|
||||||
|
|
||||||
|
String invalidRequestMessage;
|
||||||
|
|
||||||
String clientId;
|
String clientId;
|
||||||
String redirectUriParam;
|
String redirectUriParam;
|
||||||
String responseType;
|
String responseType;
|
||||||
|
@ -120,4 +122,8 @@ public class AuthorizationEndpointRequest {
|
||||||
public String getDisplay() {
|
public String getDisplay() {
|
||||||
return display;
|
return display;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getInvalidRequestMessage() {
|
||||||
|
return invalidRequestMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.keycloak.services.messages.Messages;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -43,7 +44,13 @@ public class AuthorizationEndpointRequestParserProcessor {
|
||||||
try {
|
try {
|
||||||
AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
|
AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
|
||||||
|
|
||||||
new AuthzEndpointQueryStringParser(requestParams).parseRequest(request);
|
AuthzEndpointQueryStringParser parser = new AuthzEndpointQueryStringParser(requestParams);
|
||||||
|
parser.parseRequest(request);
|
||||||
|
|
||||||
|
if (parser.getInvalidRequestMessage() != null) {
|
||||||
|
request.invalidRequestMessage = parser.getInvalidRequestMessage();
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM);
|
String requestParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_PARAM);
|
||||||
String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
String requestUriParam = requestParams.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
|
||||||
|
@ -83,4 +90,15 @@ public class AuthorizationEndpointRequestParserProcessor {
|
||||||
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getClientId(EventBuilder event, KeycloakSession session, MultivaluedMap<String, String> requestParams) {
|
||||||
|
List<String> clientParam = requestParams.get(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||||
|
if (clientParam != null && clientParam.size() == 1) {
|
||||||
|
return clientParam.get(0);
|
||||||
|
} else {
|
||||||
|
event.error(Errors.INVALID_REQUEST);
|
||||||
|
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.protocol.oidc.endpoints.request;
|
package org.keycloak.protocol.oidc.endpoints.request;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,23 +30,40 @@ class AuthzEndpointQueryStringParser extends AuthzEndpointRequestParser {
|
||||||
|
|
||||||
private final MultivaluedMap<String, String> requestParams;
|
private final MultivaluedMap<String, String> requestParams;
|
||||||
|
|
||||||
|
private String invalidRequestMessage = null;
|
||||||
|
|
||||||
public AuthzEndpointQueryStringParser(MultivaluedMap<String, String> requestParams) {
|
public AuthzEndpointQueryStringParser(MultivaluedMap<String, String> requestParams) {
|
||||||
this.requestParams = requestParams;
|
this.requestParams = requestParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String getParameter(String paramName) {
|
protected String getParameter(String paramName) {
|
||||||
|
checkDuplicated(requestParams, paramName);
|
||||||
return requestParams.getFirst(paramName);
|
return requestParams.getFirst(paramName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Integer getIntParameter(String paramName) {
|
protected Integer getIntParameter(String paramName) {
|
||||||
|
checkDuplicated(requestParams, paramName);
|
||||||
String paramVal = requestParams.getFirst(paramName);
|
String paramVal = requestParams.getFirst(paramName);
|
||||||
return paramVal==null ? null : Integer.parseInt(paramVal);
|
return paramVal==null ? null : Integer.parseInt(paramVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getInvalidRequestMessage() {
|
||||||
|
return invalidRequestMessage;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Set<String> keySet() {
|
protected Set<String> keySet() {
|
||||||
return requestParams.keySet();
|
return requestParams.keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkDuplicated(MultivaluedMap<String, String> requestParams, String paramName) {
|
||||||
|
if (invalidRequestMessage == null) {
|
||||||
|
if (requestParams.get(paramName) != null && requestParams.get(paramName).size() != 1) {
|
||||||
|
invalidRequestMessage = "duplicated parameter";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,10 +70,8 @@ abstract class AuthzEndpointRequestParser {
|
||||||
// https://tools.ietf.org/html/rfc7636#section-6.1
|
// https://tools.ietf.org/html/rfc7636#section-6.1
|
||||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
|
||||||
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM);
|
KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void parseRequest(AuthorizationEndpointRequest request) {
|
public void parseRequest(AuthorizationEndpointRequest request) {
|
||||||
String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
String clientId = getParameter(OIDCLoginProtocol.CLIENT_ID_PARAM);
|
||||||
|
|
||||||
|
@ -104,7 +102,6 @@ abstract class AuthzEndpointRequestParser {
|
||||||
extractAdditionalReqParams(request.additionalReqParams);
|
extractAdditionalReqParams(request.additionalReqParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void extractAdditionalReqParams(Map<String, String> additionalReqParams) {
|
protected void extractAdditionalReqParams(Map<String, String> additionalReqParams) {
|
||||||
for (String paramName : keySet()) {
|
for (String paramName : keySet()) {
|
||||||
if (!KNOWN_REQ_PARAMS.contains(paramName)) {
|
if (!KNOWN_REQ_PARAMS.contains(paramName)) {
|
||||||
|
@ -130,7 +127,6 @@ abstract class AuthzEndpointRequestParser {
|
||||||
return newVal==null ? previousVal : newVal;
|
return newVal==null ? previousVal : newVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected abstract String getParameter(String paramName);
|
protected abstract String getParameter(String paramName);
|
||||||
|
|
||||||
protected abstract Integer getIntParameter(String paramName);
|
protected abstract Integer getIntParameter(String paramName);
|
||||||
|
|
|
@ -157,6 +157,8 @@ public class OAuthClient {
|
||||||
private String codeChallengeMethod;
|
private String codeChallengeMethod;
|
||||||
private String origin;
|
private String origin;
|
||||||
|
|
||||||
|
private Map<String, String> customParameters;
|
||||||
|
|
||||||
private boolean openid = true;
|
private boolean openid = true;
|
||||||
|
|
||||||
private Supplier<CloseableHttpClient> httpClient = OAuthClient::newCloseableHttpClient;
|
private Supplier<CloseableHttpClient> httpClient = OAuthClient::newCloseableHttpClient;
|
||||||
|
@ -222,6 +224,7 @@ public class OAuthClient {
|
||||||
codeChallenge = null;
|
codeChallenge = null;
|
||||||
codeChallengeMethod = null;
|
codeChallengeMethod = null;
|
||||||
origin = null;
|
origin = null;
|
||||||
|
customParameters = null;
|
||||||
openid = true;
|
openid = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -884,7 +887,11 @@ public class OAuthClient {
|
||||||
}
|
}
|
||||||
if (codeChallengeMethod != null) {
|
if (codeChallengeMethod != null) {
|
||||||
b.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, codeChallengeMethod);
|
b.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, codeChallengeMethod);
|
||||||
}
|
}
|
||||||
|
if (customParameters != null) {
|
||||||
|
customParameters.keySet().stream().forEach(i -> b.queryParam(i, customParameters.get(i)));
|
||||||
|
}
|
||||||
|
|
||||||
return b.build(realm).toString();
|
return b.build(realm).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1053,6 +1060,14 @@ public class OAuthClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OAuthClient addCustomerParameter(String key, String value) {
|
||||||
|
if (customParameters == null) {
|
||||||
|
customParameters = new HashMap<>();
|
||||||
|
}
|
||||||
|
customParameters.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public static class AuthorizationEndpointResponse {
|
public static class AuthorizationEndpointResponse {
|
||||||
|
|
||||||
private boolean isRedirected;
|
private boolean isRedirected;
|
||||||
|
|
|
@ -16,13 +16,13 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.oauth;
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.OAuthErrorException;
|
import org.keycloak.OAuthErrorException;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
|
@ -30,6 +30,7 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.PageUtils;
|
import org.keycloak.testsuite.pages.PageUtils;
|
||||||
import org.keycloak.testsuite.util.ClientManager;
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
@ -38,9 +39,12 @@ import org.openqa.selenium.By;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,6 +55,9 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private ErrorPage errorPage;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||||
|
@ -70,7 +77,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
Assert.assertTrue(response.isRedirected());
|
assertTrue(response.isRedirected());
|
||||||
Assert.assertNotNull(response.getCode());
|
Assert.assertNotNull(response.getCode());
|
||||||
assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", response.getState());
|
assertEquals("OpenIdConnect.AuthenticationProperties=2302984sdlk", response.getState());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -101,7 +108,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
Assert.assertTrue(response.isRedirected());
|
assertTrue(response.isRedirected());
|
||||||
Assert.assertNotNull(response.getCode());
|
Assert.assertNotNull(response.getCode());
|
||||||
|
|
||||||
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
String codeId = events.expectLogin().assertEvent().getDetails().get(Details.CODE_ID);
|
||||||
|
@ -113,7 +120,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
Assert.assertTrue(response.isRedirected());
|
assertTrue(response.isRedirected());
|
||||||
Assert.assertNotNull(response.getCode());
|
Assert.assertNotNull(response.getCode());
|
||||||
Assert.assertNull(response.getState());
|
Assert.assertNull(response.getState());
|
||||||
Assert.assertNull(response.getError());
|
Assert.assertNull(response.getError());
|
||||||
|
@ -128,7 +135,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
driver.navigate().to(b.build().toURL());
|
driver.navigate().to(b.build().toURL());
|
||||||
|
|
||||||
OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
|
OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
|
||||||
Assert.assertTrue(errorResponse.isRedirected());
|
assertTrue(errorResponse.isRedirected());
|
||||||
Assert.assertEquals(errorResponse.getError(), OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE);
|
Assert.assertEquals(errorResponse.getError(), OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE);
|
||||||
|
|
||||||
events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "tokenn").assertEvent();
|
events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().detail(Details.RESPONSE_TYPE, "tokenn").assertEvent();
|
||||||
|
@ -197,4 +204,39 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
||||||
Assert.assertNull(currentUri.getRawFragment());
|
Assert.assertNull(currentUri.getRawFragment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizationRequestParamsMoreThanOnce() throws IOException {
|
||||||
|
oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
|
||||||
|
Map<String, String> extraParams = new HashMap<>();
|
||||||
|
|
||||||
|
oauth.addCustomerParameter(OAuth2Constants.SCOPE, "read_write")
|
||||||
|
.addCustomerParameter(OAuth2Constants.STATE, "abcdefg")
|
||||||
|
.addCustomerParameter(OAuth2Constants.SCOPE, "pop push");
|
||||||
|
|
||||||
|
oauth.openLoginForm();
|
||||||
|
|
||||||
|
assertEquals("invalid_request", oauth.getCurrentQuery().get("error"));
|
||||||
|
assertEquals("duplicated parameter", oauth.getCurrentQuery().get("error_description"));
|
||||||
|
|
||||||
|
events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void authorizationRequestClientParamsMoreThanOnce() throws IOException {
|
||||||
|
oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
|
||||||
|
|
||||||
|
oauth.addCustomerParameter(OAuth2Constants.SCOPE, "read_write")
|
||||||
|
.addCustomerParameter(OAuth2Constants.CLIENT_ID, "client2client")
|
||||||
|
.addCustomerParameter(OAuth2Constants.REDIRECT_URI, "https://www.example.com")
|
||||||
|
.addCustomerParameter(OAuth2Constants.STATE, "abcdefg")
|
||||||
|
.addCustomerParameter(OAuth2Constants.SCOPE, "pop push");
|
||||||
|
|
||||||
|
oauth.openLoginForm();
|
||||||
|
|
||||||
|
assertTrue(errorPage.isCurrent());
|
||||||
|
assertEquals("Invalid Request", errorPage.getError());
|
||||||
|
|
||||||
|
events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).client((String) null).clearDetails().assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue